{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "62b3e60b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "f1b2cf07",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import gc\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import re\n",
    "import time\n",
    "from scipy import stats\n",
    "import matplotlib.pyplot as plt\n",
    "import category_encoders as ce\n",
    "import networkx as nx\n",
    "import pickle\n",
    "import lightgbm as lgb\n",
    "import catboost as cat\n",
    "import xgboost as xgb\n",
    "from datetime import timedelta\n",
    "from gensim.models import Word2Vec\n",
    "from io import StringIO\n",
    "from tqdm import tqdm\n",
    "from lightgbm import LGBMClassifier\n",
    "from lightgbm import log_evaluation, early_stopping\n",
    "from sklearn.metrics import roc_curve\n",
    "from scipy.stats import chi2_contingency, pearsonr\n",
    "from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer\n",
    "from sklearn.feature_extraction import FeatureHasher\n",
    "from sklearn.model_selection import StratifiedKFold, KFold, train_test_split, GridSearchCV\n",
    "from category_encoders import TargetEncoder\n",
    "from sklearn.decomposition import TruncatedSVD\n",
    "from autogluon.tabular import TabularDataset, TabularPredictor, FeatureMetadata\n",
    "from autogluon.features.generators import AsTypeFeatureGenerator, BulkFeatureGenerator, DropUniqueFeatureGenerator, FillNaFeatureGenerator, PipelineFeatureGenerator\n",
    "from autogluon.features.generators import CategoryFeatureGenerator, IdentityFeatureGenerator, AutoMLPipelineFeatureGenerator\n",
    "from autogluon.common.features.types import R_INT, R_FLOAT\n",
    "from autogluon.core.metrics import make_scorer"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "56d1c0f9",
   "metadata": {},
   "source": [
    "# 数据导入"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7529f127",
   "metadata": {},
   "source": [
    "## 通用导入函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "0b7021c6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def load_data_from_directory(directory):\n",
    "    \"\"\"\n",
    "    遍历目录加载所有CSV文件，将其作为独立的DataFrame变量\n",
    "\n",
    "    参数:\n",
    "    - directory: 输入的数据路径\n",
    "    \n",
    "    返回:\n",
    "    - 含有数据集名称的列表\n",
    "    \"\"\"\n",
    "    dataset_names = []\n",
    "    for filename in os.listdir(directory):\n",
    "        if filename.endswith(\".csv\"):\n",
    "            dataset_name = os.path.splitext(filename)[0] + '_data' # 获取文件名作为变量名\n",
    "            file_path = os.path.join(directory, filename)  # 完整的文件路径\n",
    "            globals()[dataset_name] = pd.read_csv(file_path)  # 将文件加载为DataFrame并赋值给全局变量\n",
    "            dataset_names.append(dataset_name)\n",
    "            print(f\"数据集 {dataset_name} 已加载为 DataFrame\")\n",
    "\n",
    "    return dataset_names"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8434568a",
   "metadata": {},
   "source": [
    "## 训练集导入"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "c410a626",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据集 AGET_PAY_data 已加载为 DataFrame\n",
      "数据集 ASSET_data 已加载为 DataFrame\n",
      "数据集 CCD_TR_DTL_data 已加载为 DataFrame\n",
      "数据集 MB_PAGEVIEW_DTL_data 已加载为 DataFrame\n",
      "数据集 MB_QRYTRNFLW_data 已加载为 DataFrame\n",
      "数据集 MB_TRNFLW_data 已加载为 DataFrame\n",
      "数据集 NATURE_data 已加载为 DataFrame\n",
      "数据集 PROD_HOLD_data 已加载为 DataFrame\n",
      "数据集 TARGET_data 已加载为 DataFrame\n",
      "数据集 TARGET_VALID_data 已加载为 DataFrame\n",
      "数据集 TR_APS_DTL_data 已加载为 DataFrame\n",
      "数据集 TR_IBTF_data 已加载为 DataFrame\n",
      "数据集 TR_TPAY_data 已加载为 DataFrame\n"
     ]
    }
   ],
   "source": [
    "train_load_dt = '../DATA'\n",
    "train_data_name = load_data_from_directory(train_load_dt)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8812a6c",
   "metadata": {},
   "source": [
    "# 特征工程"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ccc077e4",
   "metadata": {},
   "source": [
    "## 分类特征处理函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "2a78da18",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "分类特征处理函数定义完成\n"
     ]
    }
   ],
   "source": [
    "def get_id_category_features(df_fea, df_to_groupby, fea1, fea2, stat):\n",
    "    \"\"\"\n",
    "    对分类特征进行分组统计\n",
    "    参数:\n",
    "        df_fea: 特征数据框\n",
    "        df_to_groupby: 待分组的数据框\n",
    "        fea1: 分组特征1(如模块名称)\n",
    "        fea2: 统计特征(如点击次数)\n",
    "        stat: 统计方法\n",
    "    \"\"\"\n",
    "    tmp = df_to_groupby.groupby(['CUST_NO', fea1])[fea2].agg(\n",
    "        stat if stat != \"kurt\" else lambda x: x.kurt()\n",
    "    ).to_frame(\n",
    "        '_'.join(['CUST_NO', fea1, fea2, stat])\n",
    "    ).reset_index()\n",
    "    \n",
    "    df_tmp = pd.pivot(data=tmp, index='CUST_NO', columns=fea1, values='_'.join(['CUST_NO', fea1, fea2, stat]))\n",
    "    new_fea_cols = ['_'.join(['CUST_NO', fea1, fea2, stat, str(col)]) for col in df_tmp.columns]\n",
    "    df_tmp.columns = new_fea_cols\n",
    "    df_tmp.reset_index(inplace=True)\n",
    "        \n",
    "    if stat == 'count':\n",
    "        df_tmp = df_tmp.fillna(0)\n",
    "        \n",
    "    # 去掉全NaN列\n",
    "    valid_cols = []\n",
    "    for col in df_tmp.columns:\n",
    "        if not df_tmp[col].isna().all():\n",
    "            valid_cols.append(col)\n",
    "            \n",
    "    df_fea = df_fea.merge(df_tmp[valid_cols], on='CUST_NO', how='left')\n",
    "    return df_fea, new_fea_cols \n",
    "\n",
    "def get_all_id_category_features(df_fea, df_to_groupby, fea1, fea2, stats):\n",
    "    \"\"\"批量对分类特征进行多种统计\"\"\"\n",
    "    all_new_fea_cols = []\n",
    "    for stat in tqdm(stats, desc=f'Processing {fea1}-{fea2}'):\n",
    "        df_fea, new_fea_cols = get_id_category_features(df_fea, df_to_groupby, fea1, fea2, stat)\n",
    "        all_new_fea_cols += new_fea_cols\n",
    "    return df_fea, all_new_fea_cols\n",
    "\n",
    "print(\"分类特征处理函数定义完成\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e563753",
   "metadata": {},
   "source": [
    "## 基础时间特征函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "1f0bb1ce",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据形状: (372196, 14)\n",
      "时间范围: 2025-04-01 00:00:00 ~ 2025-06-30 00:00:00\n",
      "月份分布:\n",
      "date_months_to_now\n",
      "0    132174\n",
      "1    122815\n",
      "2    117207\n",
      "Name: count, dtype: int64\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>OPERATION_DATE</th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>PAGE_TITLE</th>\n",
       "      <th>REFERRER_TITLE</th>\n",
       "      <th>MODEL_NAME</th>\n",
       "      <th>date_months_to_now</th>\n",
       "      <th>date_weeks_to_now</th>\n",
       "      <th>date_days_to_now</th>\n",
       "      <th>operation_month</th>\n",
       "      <th>operation_day</th>\n",
       "      <th>operation_dayofweek</th>\n",
       "      <th>operation_is_weekend</th>\n",
       "      <th>operation_is_month_start</th>\n",
       "      <th>operation_is_month_end</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2025-04-07</td>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>dc127d306179477fef4f3a9378dc550b</td>\n",
       "      <td>0e5c9561153e8b3fd936b94a5641c8e1</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>2</td>\n",
       "      <td>12</td>\n",
       "      <td>84</td>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2025-04-07</td>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>0e5c9561153e8b3fd936b94a5641c8e1</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>2</td>\n",
       "      <td>12</td>\n",
       "      <td>84</td>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2025-04-07</td>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>a3efea933884689e89b46cadd9aa989e</td>\n",
       "      <td>dc127d306179477fef4f3a9378dc550b</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>2</td>\n",
       "      <td>12</td>\n",
       "      <td>84</td>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2025-04-07</td>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>dc127d306179477fef4f3a9378dc550b</td>\n",
       "      <td>a3efea933884689e89b46cadd9aa989e</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>2</td>\n",
       "      <td>12</td>\n",
       "      <td>84</td>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2025-04-07</td>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>dc127d306179477fef4f3a9378dc550b</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>c5b386b7a6348a2f1ba70f2259fb827e</td>\n",
       "      <td>2</td>\n",
       "      <td>12</td>\n",
       "      <td>84</td>\n",
       "      <td>4</td>\n",
       "      <td>7</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "      <td>0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "  OPERATION_DATE                           CUST_NO  \\\n",
       "0     2025-04-07  864a14a62ffffbc4741d365ea5a08278   \n",
       "1     2025-04-07  864a14a62ffffbc4741d365ea5a08278   \n",
       "2     2025-04-07  864a14a62ffffbc4741d365ea5a08278   \n",
       "3     2025-04-07  74dfe9a67327540d1f427b40e85d49c7   \n",
       "4     2025-04-07  74dfe9a67327540d1f427b40e85d49c7   \n",
       "\n",
       "                         PAGE_TITLE                    REFERRER_TITLE  \\\n",
       "0  dc127d306179477fef4f3a9378dc550b  0e5c9561153e8b3fd936b94a5641c8e1   \n",
       "1  0e5c9561153e8b3fd936b94a5641c8e1  c5b386b7a6348a2f1ba70f2259fb827e   \n",
       "2  a3efea933884689e89b46cadd9aa989e  dc127d306179477fef4f3a9378dc550b   \n",
       "3  dc127d306179477fef4f3a9378dc550b  a3efea933884689e89b46cadd9aa989e   \n",
       "4  dc127d306179477fef4f3a9378dc550b  c5b386b7a6348a2f1ba70f2259fb827e   \n",
       "\n",
       "                         MODEL_NAME  date_months_to_now  date_weeks_to_now  \\\n",
       "0  c5b386b7a6348a2f1ba70f2259fb827e                   2                 12   \n",
       "1  c5b386b7a6348a2f1ba70f2259fb827e                   2                 12   \n",
       "2  c5b386b7a6348a2f1ba70f2259fb827e                   2                 12   \n",
       "3  c5b386b7a6348a2f1ba70f2259fb827e                   2                 12   \n",
       "4  c5b386b7a6348a2f1ba70f2259fb827e                   2                 12   \n",
       "\n",
       "   date_days_to_now  operation_month  operation_day  operation_dayofweek  \\\n",
       "0                84                4              7                    0   \n",
       "1                84                4              7                    0   \n",
       "2                84                4              7                    0   \n",
       "3                84                4              7                    0   \n",
       "4                84                4              7                    0   \n",
       "\n",
       "   operation_is_weekend  operation_is_month_start  operation_is_month_end  \n",
       "0                     0                         0                       0  \n",
       "1                     0                         0                       0  \n",
       "2                     0                         0                       0  \n",
       "3                     0                         0                       0  \n",
       "4                     0                         0                       0  "
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 1. 时间特征工程\n",
    "def get_days_to_now(df):\n",
    "    \"\"\"将操作日期转换为距今天数\"\"\"\n",
    "    df = df.copy()\n",
    "    # 自动检测日期格式\n",
    "    if df[\"OPERATION_DATE\"].dtype == 'object':\n",
    "        # 尝试不同的日期格式\n",
    "        try:\n",
    "            df[\"OPERATION_DATE\"] = pd.to_datetime(df[\"OPERATION_DATE\"], format=\"%Y%m%d\")\n",
    "        except:\n",
    "            df[\"OPERATION_DATE\"] = pd.to_datetime(df[\"OPERATION_DATE\"])\n",
    "    \n",
    "    df_days_to_now = (df[\"OPERATION_DATE\"].max() - df[\"OPERATION_DATE\"]).dt.days\n",
    "    df[\"date_months_to_now\"] = df_days_to_now // 31  # 距今月数\n",
    "    df[\"date_weeks_to_now\"] = df_days_to_now // 7   # 距今周数\n",
    "    df[\"date_days_to_now\"] = df_days_to_now          # 距今天数\n",
    "    \n",
    "    # 提取时间维度特征\n",
    "    df[\"operation_month\"] = df[\"OPERATION_DATE\"].dt.month\n",
    "    df[\"operation_day\"] = df[\"OPERATION_DATE\"].dt.day\n",
    "    df[\"operation_dayofweek\"] = df[\"OPERATION_DATE\"].dt.dayofweek  # 周几\n",
    "    df[\"operation_is_weekend\"] = df[\"operation_dayofweek\"].isin([5, 6]).astype(int)\n",
    "    df[\"operation_is_month_start\"] = df[\"OPERATION_DATE\"].dt.is_month_start.astype(int)\n",
    "    df[\"operation_is_month_end\"] = df[\"OPERATION_DATE\"].dt.is_month_end.astype(int)\n",
    "    \n",
    "    return df\n",
    "\n",
    "# 处理数据\n",
    "MB_PAGEVIEW_DTL_data = get_days_to_now(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"数据形状: {MB_PAGEVIEW_DTL_data.shape}\")\n",
    "print(f\"时间范围: {MB_PAGEVIEW_DTL_data['OPERATION_DATE'].min()} ~ {MB_PAGEVIEW_DTL_data['OPERATION_DATE'].max()}\")\n",
    "print(f\"月份分布:\\n{MB_PAGEVIEW_DTL_data['date_months_to_now'].value_counts().sort_index()}\")\n",
    "MB_PAGEVIEW_DTL_data.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "79a86275",
   "metadata": {},
   "source": [
    "## RFM特征函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "9701c1d2",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "RFM-R特征: 100%|██████████| 3/3 [00:00<00:00, 93.22it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 基础RFM特征数量: 18\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>max_nunique_days_to_now_0</th>\n",
       "      <th>max_count_days_to_now_0</th>\n",
       "      <th>max_nunique_days_to_now_1</th>\n",
       "      <th>max_count_days_to_now_1</th>\n",
       "      <th>max_nunique_days_to_now_2</th>\n",
       "      <th>max_count_days_to_now_2</th>\n",
       "      <th>daily_page_nunique_mean</th>\n",
       "      <th>daily_page_count_mean</th>\n",
       "      <th>daily_page_nunique_max</th>\n",
       "      <th>daily_page_count_max</th>\n",
       "      <th>daily_page_nunique_min</th>\n",
       "      <th>daily_page_count_min</th>\n",
       "      <th>daily_page_nunique_median</th>\n",
       "      <th>daily_page_count_median</th>\n",
       "      <th>daily_page_nunique_std</th>\n",
       "      <th>daily_page_count_std</th>\n",
       "      <th>daily_page_nunique_sum</th>\n",
       "      <th>daily_page_count_sum</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>76.0</td>\n",
       "      <td>76.0</td>\n",
       "      <td>3.250000</td>\n",
       "      <td>4.666667</td>\n",
       "      <td>6</td>\n",
       "      <td>12</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "      <td>3.0</td>\n",
       "      <td>3.5</td>\n",
       "      <td>0.866025</td>\n",
       "      <td>2.774341</td>\n",
       "      <td>39</td>\n",
       "      <td>56</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>5.0</td>\n",
       "      <td>26.0</td>\n",
       "      <td>51.0</td>\n",
       "      <td>56.0</td>\n",
       "      <td>67.0</td>\n",
       "      <td>67.0</td>\n",
       "      <td>3.739130</td>\n",
       "      <td>5.782609</td>\n",
       "      <td>7</td>\n",
       "      <td>14</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "      <td>3.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>1.053884</td>\n",
       "      <td>3.044466</td>\n",
       "      <td>86</td>\n",
       "      <td>133</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>15.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>60.0</td>\n",
       "      <td>42.0</td>\n",
       "      <td>73.0</td>\n",
       "      <td>84.0</td>\n",
       "      <td>4.500000</td>\n",
       "      <td>7.500000</td>\n",
       "      <td>9</td>\n",
       "      <td>19</td>\n",
       "      <td>2</td>\n",
       "      <td>3</td>\n",
       "      <td>4.0</td>\n",
       "      <td>6.5</td>\n",
       "      <td>1.303840</td>\n",
       "      <td>3.569314</td>\n",
       "      <td>117</td>\n",
       "      <td>195</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>4.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>45.0</td>\n",
       "      <td>42.0</td>\n",
       "      <td>68.0</td>\n",
       "      <td>68.0</td>\n",
       "      <td>3.904762</td>\n",
       "      <td>6.619048</td>\n",
       "      <td>7</td>\n",
       "      <td>24</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "      <td>4.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>1.179185</td>\n",
       "      <td>5.103687</td>\n",
       "      <td>82</td>\n",
       "      <td>139</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>27.0</td>\n",
       "      <td>26.0</td>\n",
       "      <td>38.0</td>\n",
       "      <td>47.0</td>\n",
       "      <td>81.0</td>\n",
       "      <td>75.0</td>\n",
       "      <td>6.125000</td>\n",
       "      <td>10.343750</td>\n",
       "      <td>16</td>\n",
       "      <td>33</td>\n",
       "      <td>3</td>\n",
       "      <td>4</td>\n",
       "      <td>4.5</td>\n",
       "      <td>6.0</td>\n",
       "      <td>3.480360</td>\n",
       "      <td>8.334187</td>\n",
       "      <td>196</td>\n",
       "      <td>331</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  max_nunique_days_to_now_0  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278                        NaN   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7                        5.0   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c                       15.0   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f                        4.0   \n",
       "4  3f60ef34ad3853819e4269469280177d                       27.0   \n",
       "\n",
       "   max_count_days_to_now_0  max_nunique_days_to_now_1  \\\n",
       "0                      NaN                        NaN   \n",
       "1                     26.0                       51.0   \n",
       "2                     15.0                       60.0   \n",
       "3                      4.0                       45.0   \n",
       "4                     26.0                       38.0   \n",
       "\n",
       "   max_count_days_to_now_1  max_nunique_days_to_now_2  \\\n",
       "0                      NaN                       76.0   \n",
       "1                     56.0                       67.0   \n",
       "2                     42.0                       73.0   \n",
       "3                     42.0                       68.0   \n",
       "4                     47.0                       81.0   \n",
       "\n",
       "   max_count_days_to_now_2  daily_page_nunique_mean  daily_page_count_mean  \\\n",
       "0                     76.0                 3.250000               4.666667   \n",
       "1                     67.0                 3.739130               5.782609   \n",
       "2                     84.0                 4.500000               7.500000   \n",
       "3                     68.0                 3.904762               6.619048   \n",
       "4                     75.0                 6.125000              10.343750   \n",
       "\n",
       "   daily_page_nunique_max  daily_page_count_max  daily_page_nunique_min  \\\n",
       "0                       6                    12                       3   \n",
       "1                       7                    14                       3   \n",
       "2                       9                    19                       2   \n",
       "3                       7                    24                       3   \n",
       "4                      16                    33                       3   \n",
       "\n",
       "   daily_page_count_min  daily_page_nunique_median  daily_page_count_median  \\\n",
       "0                     3                        3.0                      3.5   \n",
       "1                     3                        3.0                      5.0   \n",
       "2                     3                        4.0                      6.5   \n",
       "3                     3                        4.0                      4.0   \n",
       "4                     4                        4.5                      6.0   \n",
       "\n",
       "   daily_page_nunique_std  daily_page_count_std  daily_page_nunique_sum  \\\n",
       "0                0.866025              2.774341                      39   \n",
       "1                1.053884              3.044466                      86   \n",
       "2                1.303840              3.569314                     117   \n",
       "3                1.179185              5.103687                      82   \n",
       "4                3.480360              8.334187                     196   \n",
       "\n",
       "   daily_page_count_sum  \n",
       "0                    56  \n",
       "1                   133  \n",
       "2                   195  \n",
       "3                   139  \n",
       "4                   331  "
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 2. RFM特征: 按天聚合统计\n",
    "def gen_mb_basic_features(df):\n",
    "    \"\"\"\n",
    "    生成掌银页面访问的基础RFM特征\n",
    "    - R (Recency): 最近访问时间\n",
    "    - F (Frequency): 访问频次\n",
    "    - M (Monetary): 访问页面种类\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 按天聚合\n",
    "    df_by_day = df.groupby([\"CUST_NO\", \"date_days_to_now\", \"date_weeks_to_now\", \"date_months_to_now\"])[\"PAGE_TITLE\"].agg(['nunique', 'count'])\n",
    "    df_by_day.columns = ['PAGE_TITLE_nunique', 'PAGE_TITLE_count']\n",
    "    df_by_day = df_by_day.reset_index()\n",
    "    \n",
    "    # RFM-R: 每月日点击笔数/日点击页面数最大天距今天数\n",
    "    def get_max_cnt_days_to_now(df_month, month):\n",
    "        # 日点击页面数最多的那天距今天数\n",
    "        tmp_df_nunique = df_month.groupby(['CUST_NO']).agg({\"PAGE_TITLE_nunique\": \"max\"}).reset_index()\n",
    "        tmp_df_nunique = tmp_df_nunique.merge(\n",
    "            df_month[['CUST_NO', 'date_days_to_now', 'PAGE_TITLE_nunique']], \n",
    "            on=[\"CUST_NO\", 'PAGE_TITLE_nunique'], how=\"inner\"\n",
    "        )\n",
    "        tmp_df_nunique = tmp_df_nunique.groupby(['CUST_NO'])[\"date_days_to_now\"].min().to_frame(\n",
    "            f\"max_nunique_days_to_now_{month}\"\n",
    "        ).reset_index()\n",
    "        \n",
    "        # 日点击次数最多的那天距今天数\n",
    "        tmp_df_cnt = df_month.groupby(['CUST_NO']).agg({\"PAGE_TITLE_count\": \"max\"}).reset_index()\n",
    "        tmp_df_cnt = tmp_df_cnt.merge(\n",
    "            df_month[['CUST_NO', 'date_days_to_now', 'PAGE_TITLE_count']], \n",
    "            on=[\"CUST_NO\", 'PAGE_TITLE_count'], how=\"inner\"\n",
    "        )\n",
    "        tmp_df_cnt = tmp_df_cnt.groupby(['CUST_NO'])[\"date_days_to_now\"].min().to_frame(\n",
    "            f\"max_count_days_to_now_{month}\"\n",
    "        ).reset_index()\n",
    "        \n",
    "        return tmp_df_nunique, tmp_df_cnt\n",
    "    \n",
    "    # 分月统计\n",
    "    for month in tqdm([0, 1, 2], desc='RFM-R特征'):\n",
    "        data_month = df_by_day[df_by_day[\"date_months_to_now\"] == month]\n",
    "        tmp_df_nunique, tmp_df_cnt = get_max_cnt_days_to_now(data_month, month)\n",
    "        feature = feature.merge(tmp_df_nunique, how=\"left\", on=\"CUST_NO\")\n",
    "        feature = feature.merge(tmp_df_cnt, how=\"left\", on=\"CUST_NO\")\n",
    "    \n",
    "    # RFM-F: 整体访问频次统计\n",
    "    for stat in ['mean', 'max', 'min', 'median', 'std', 'sum']:\n",
    "        # 每日点击页面数统计\n",
    "        tmp = df_by_day.groupby(['CUST_NO'])['PAGE_TITLE_nunique'].agg(stat).to_frame(\n",
    "            f'daily_page_nunique_{stat}'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 每日点击次数统计\n",
    "        tmp = df_by_day.groupby(['CUST_NO'])['PAGE_TITLE_count'].agg(stat).to_frame(\n",
    "            f'daily_page_count_{stat}'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_basic_features = gen_mb_basic_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 基础RFM特征数量: {mb_basic_features.shape[1] - 1}\")  # 减去CUST_NO\n",
    "mb_basic_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "132e6fdd",
   "metadata": {},
   "source": [
    "## 页面/模块访问路径特征函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "294760e1",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Top页面路径特征: 100%|██████████| 30/30 [00:00<00:00, 78.56it/s]\n",
      "Top模块路径特征: 100%|██████████| 30/30 [00:00<00:00, 73.19it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 导航路径特征数量: 66\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>unique_page_paths</th>\n",
       "      <th>total_page_visits</th>\n",
       "      <th>page_path_diversity</th>\n",
       "      <th>unique_model_paths</th>\n",
       "      <th>total_model_visits</th>\n",
       "      <th>model_path_diversity</th>\n",
       "      <th>page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_dc127d306179477fef4f3a9378dc550b</th>\n",
       "      <th>page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_0e5c9561153e8b3fd936b94a5641c8e1</th>\n",
       "      <th>page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_cbbf501be82cabb897c45e7fcb13f167</th>\n",
       "      <th>...</th>\n",
       "      <th>model_path_count_bcf724833716039656a608713cad279f_bcf724833716039656a608713cad279f</th>\n",
       "      <th>model_path_count_75c9358b1c56ab43410e6a12ade1a393_75c9358b1c56ab43410e6a12ade1a393</th>\n",
       "      <th>model_path_count_878ac3a435a9e30b63c6b4c8c1806171_b989cf3952250c20ce3f5ce391638fbc</th>\n",
       "      <th>model_path_count_d70f9f4b8952b0a2ad8879c7eb3d8813_d70f9f4b8952b0a2ad8879c7eb3d8813</th>\n",
       "      <th>model_path_count_b7d64bf59eee872f66ab190a19478e5e_878ac3a435a9e30b63c6b4c8c1806171</th>\n",
       "      <th>model_path_count_81d3e1ea5b8de51bda4e9047a5773008_81d3e1ea5b8de51bda4e9047a5773008</th>\n",
       "      <th>model_path_count_efc38e763169ffe517f414056b005d00_efc38e763169ffe517f414056b005d00</th>\n",
       "      <th>model_path_count_b989cf3952250c20ce3f5ce391638fbc_878ac3a435a9e30b63c6b4c8c1806171</th>\n",
       "      <th>model_path_count_9fea9e924bdd0d9464ab9e90138556e7_c5b386b7a6348a2f1ba70f2259fb827e</th>\n",
       "      <th>model_path_count_c5b386b7a6348a2f1ba70f2259fb827e_56e6187b5b44989eea9322ccb9443036</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>9</td>\n",
       "      <td>56</td>\n",
       "      <td>0.157895</td>\n",
       "      <td>6</td>\n",
       "      <td>56</td>\n",
       "      <td>0.105263</td>\n",
       "      <td>NaN</td>\n",
       "      <td>15.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>25</td>\n",
       "      <td>133</td>\n",
       "      <td>0.186567</td>\n",
       "      <td>13</td>\n",
       "      <td>133</td>\n",
       "      <td>0.097015</td>\n",
       "      <td>23.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>2.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>32</td>\n",
       "      <td>195</td>\n",
       "      <td>0.163265</td>\n",
       "      <td>18</td>\n",
       "      <td>195</td>\n",
       "      <td>0.091837</td>\n",
       "      <td>10.0</td>\n",
       "      <td>16.0</td>\n",
       "      <td>2.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>1.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>25</td>\n",
       "      <td>139</td>\n",
       "      <td>0.178571</td>\n",
       "      <td>15</td>\n",
       "      <td>139</td>\n",
       "      <td>0.107143</td>\n",
       "      <td>11.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>3.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>2.0</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>69</td>\n",
       "      <td>331</td>\n",
       "      <td>0.207831</td>\n",
       "      <td>27</td>\n",
       "      <td>331</td>\n",
       "      <td>0.081325</td>\n",
       "      <td>27.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>...</td>\n",
       "      <td>2.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 67 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  unique_page_paths  total_page_visits  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278                  9                 56   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7                 25                133   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c                 32                195   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f                 25                139   \n",
       "4  3f60ef34ad3853819e4269469280177d                 69                331   \n",
       "\n",
       "   page_path_diversity  unique_model_paths  total_model_visits  \\\n",
       "0             0.157895                   6                  56   \n",
       "1             0.186567                  13                 133   \n",
       "2             0.163265                  18                 195   \n",
       "3             0.178571                  15                 139   \n",
       "4             0.207831                  27                 331   \n",
       "\n",
       "   model_path_diversity  \\\n",
       "0              0.105263   \n",
       "1              0.097015   \n",
       "2              0.091837   \n",
       "3              0.107143   \n",
       "4              0.081325   \n",
       "\n",
       "   page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_dc127d306179477fef4f3a9378dc550b  \\\n",
       "0                                                NaN                                   \n",
       "1                                               23.0                                   \n",
       "2                                               10.0                                   \n",
       "3                                               11.0                                   \n",
       "4                                               27.0                                   \n",
       "\n",
       "   page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_0e5c9561153e8b3fd936b94a5641c8e1  \\\n",
       "0                                               15.0                                   \n",
       "1                                                NaN                                   \n",
       "2                                               16.0                                   \n",
       "3                                               11.0                                   \n",
       "4                                               12.0                                   \n",
       "\n",
       "   page_path_count_c5b386b7a6348a2f1ba70f2259fb827e_cbbf501be82cabb897c45e7fcb13f167  \\\n",
       "0                                                NaN                                   \n",
       "1                                                NaN                                   \n",
       "2                                                2.0                                   \n",
       "3                                                NaN                                   \n",
       "4                                                1.0                                   \n",
       "\n",
       "   ...  \\\n",
       "0  ...   \n",
       "1  ...   \n",
       "2  ...   \n",
       "3  ...   \n",
       "4  ...   \n",
       "\n",
       "   model_path_count_bcf724833716039656a608713cad279f_bcf724833716039656a608713cad279f  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                2.0                                    \n",
       "\n",
       "   model_path_count_75c9358b1c56ab43410e6a12ade1a393_75c9358b1c56ab43410e6a12ade1a393  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_878ac3a435a9e30b63c6b4c8c1806171_b989cf3952250c20ce3f5ce391638fbc  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_d70f9f4b8952b0a2ad8879c7eb3d8813_d70f9f4b8952b0a2ad8879c7eb3d8813  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                3.0                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_b7d64bf59eee872f66ab190a19478e5e_878ac3a435a9e30b63c6b4c8c1806171  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_81d3e1ea5b8de51bda4e9047a5773008_81d3e1ea5b8de51bda4e9047a5773008  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_efc38e763169ffe517f414056b005d00_efc38e763169ffe517f414056b005d00  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_b989cf3952250c20ce3f5ce391638fbc_878ac3a435a9e30b63c6b4c8c1806171  \\\n",
       "0                                                NaN                                    \n",
       "1                                                2.0                                    \n",
       "2                                                1.0                                    \n",
       "3                                                NaN                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_9fea9e924bdd0d9464ab9e90138556e7_c5b386b7a6348a2f1ba70f2259fb827e  \\\n",
       "0                                                NaN                                    \n",
       "1                                                NaN                                    \n",
       "2                                                NaN                                    \n",
       "3                                                2.0                                    \n",
       "4                                                NaN                                    \n",
       "\n",
       "   model_path_count_c5b386b7a6348a2f1ba70f2259fb827e_56e6187b5b44989eea9322ccb9443036  \n",
       "0                                                NaN                                   \n",
       "1                                                NaN                                   \n",
       "2                                                NaN                                   \n",
       "3                                                NaN                                   \n",
       "4                                                NaN                                   \n",
       "\n",
       "[5 rows x 67 columns]"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 3. 页面/模块访问路径特征\n",
    "def gen_mb_navigation_path_features(df):\n",
    "    \"\"\"\n",
    "    生成页面和模块之间的跳转路径特征\n",
    "    核心思想: 从上一个页面(REFERRER_TITLE)跳转到当前页面(PAGE_TITLE)\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 3.1 创建页面跳转路径\n",
    "    df_page = df[['CUST_NO', 'REFERRER_TITLE', 'PAGE_TITLE', 'MODEL_NAME',\n",
    "                   'date_days_to_now', \"date_weeks_to_now\", 'date_months_to_now']].copy()\n",
    "    \n",
    "    # 页面路径拼接: 从REFERRER_TITLE到PAGE_TITLE\n",
    "    df_page['page_path'] = df['REFERRER_TITLE'].astype(str) + '_' + df['PAGE_TITLE'].astype(str)\n",
    "    \n",
    "    # 3.2 创建模块跳转路径 (通过页面映射到模块)\n",
    "    # 建立页面到模块的映射字典\n",
    "    page2model_dict = dict(zip(df['PAGE_TITLE'], df['MODEL_NAME']))\n",
    "    \n",
    "    df_model = df_page.copy()\n",
    "    df_model['REFERRER_MODEL_NAME'] = df_model['REFERRER_TITLE'].map(page2model_dict)\n",
    "    df_model['model_path'] = df_model['REFERRER_MODEL_NAME'].astype(str) + '_' + df_model['MODEL_NAME'].astype(str)\n",
    "    \n",
    "    # 3.3 统计各种路径的访问次数\n",
    "    # 每日页面路径访问次数\n",
    "    df_page_by_day = df_page.groupby([\n",
    "        \"CUST_NO\", \"date_days_to_now\", \"date_weeks_to_now\", \"date_months_to_now\", 'page_path'\n",
    "    ])['page_path'].agg(['count']).reset_index()\n",
    "    \n",
    "    # 每日模块路径访问次数\n",
    "    df_model_by_day = df_model.groupby([\n",
    "        \"CUST_NO\", \"date_days_to_now\", \"date_weeks_to_now\", \"date_months_to_now\", 'model_path'\n",
    "    ])['model_path'].agg(['count']).reset_index()\n",
    "    \n",
    "    # 3.4 路径多样性特征\n",
    "    # 用户访问的不同页面路径数量\n",
    "    tmp = df_page.groupby('CUST_NO')['page_path'].agg([\n",
    "        ('unique_page_paths', 'nunique'),\n",
    "        ('total_page_visits', 'count')\n",
    "    ]).reset_index()\n",
    "    tmp['page_path_diversity'] = tmp['unique_page_paths'] / (tmp['total_page_visits'] + 1)\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 用户访问的不同模块路径数量\n",
    "    tmp = df_model.groupby('CUST_NO')['model_path'].agg([\n",
    "        ('unique_model_paths', 'nunique'),\n",
    "        ('total_model_visits', 'count')\n",
    "    ]).reset_index()\n",
    "    tmp['model_path_diversity'] = tmp['unique_model_paths'] / (tmp['total_model_visits'] + 1)\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 3.5 Top路径的访问频次统计\n",
    "    # 获取最常见的页面路径\n",
    "    top_page_paths = df_page['page_path'].value_counts().head(50).index.tolist()\n",
    "    for path in tqdm(top_page_paths[:30], desc='Top页面路径特征'):  # 取前30\n",
    "        tmp = df_page[df_page['page_path'] == path].groupby('CUST_NO').size().to_frame(\n",
    "            f'page_path_count_{path}'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 获取最常见的模块路径\n",
    "    top_model_paths = df_model['model_path'].value_counts().head(50).index.tolist()\n",
    "    for path in tqdm(top_model_paths[:30], desc='Top模块路径特征'):  # 取前30\n",
    "        tmp = df_model[df_model['model_path'] == path].groupby('CUST_NO').size().to_frame(\n",
    "            f'model_path_count_{path}'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_navigation_features = gen_mb_navigation_path_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 导航路径特征数量: {mb_navigation_features.shape[1] - 1}\")\n",
    "mb_navigation_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "264bed28",
   "metadata": {},
   "source": [
    "## 页面/模块分组统计特征函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "bb30ebfa",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成页面访问统计特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "页面分组特征: 100%|██████████| 60/60 [00:01<00:00, 57.66it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成模块访问统计特征...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "模块分组特征: 100%|██████████| 40/40 [00:00<00:00, 45.07it/s]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 页面/模块分组特征数量: 300\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>page_dc127d306179477fef4f3a9378dc550b_count</th>\n",
       "      <th>page_dc127d306179477fef4f3a9378dc550b_visit_days</th>\n",
       "      <th>page_dc127d306179477fef4f3a9378dc550b_last_visit</th>\n",
       "      <th>page_a3efea933884689e89b46cadd9aa989e_count</th>\n",
       "      <th>page_a3efea933884689e89b46cadd9aa989e_visit_days</th>\n",
       "      <th>page_a3efea933884689e89b46cadd9aa989e_last_visit</th>\n",
       "      <th>page_796f39eb9f0d6e6ff63d327d96fedeb5_count</th>\n",
       "      <th>page_796f39eb9f0d6e6ff63d327d96fedeb5_visit_days</th>\n",
       "      <th>page_796f39eb9f0d6e6ff63d327d96fedeb5_last_visit</th>\n",
       "      <th>...</th>\n",
       "      <th>module_d166e7d89059242e8210b0b647206cb1_last_visit</th>\n",
       "      <th>module_2613588ce2fcc962c5511a4e3eb234ba_count</th>\n",
       "      <th>module_2613588ce2fcc962c5511a4e3eb234ba_unique_pages</th>\n",
       "      <th>module_2613588ce2fcc962c5511a4e3eb234ba_last_visit</th>\n",
       "      <th>module_1993162004f0f20fe0bcda9337e150b4_count</th>\n",
       "      <th>module_1993162004f0f20fe0bcda9337e150b4_unique_pages</th>\n",
       "      <th>module_1993162004f0f20fe0bcda9337e150b4_last_visit</th>\n",
       "      <th>module_37d09e3760ce813af229ff907157fedf_count</th>\n",
       "      <th>module_37d09e3760ce813af229ff907157fedf_unique_pages</th>\n",
       "      <th>module_37d09e3760ce813af229ff907157fedf_last_visit</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>21.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>66.0</td>\n",
       "      <td>16.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>66.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>48.0</td>\n",
       "      <td>23.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>9.0</td>\n",
       "      <td>2.0</td>\n",
       "      <td>67.0</td>\n",
       "      <td>33.0</td>\n",
       "      <td>22.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>77.0</td>\n",
       "      <td>26.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>39.0</td>\n",
       "      <td>25.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>35.0</td>\n",
       "      <td>21.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>103.0</td>\n",
       "      <td>32.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>2.0</td>\n",
       "      <td>18.0</td>\n",
       "      <td>46.0</td>\n",
       "      <td>32.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 301 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f   \n",
       "4  3f60ef34ad3853819e4269469280177d   \n",
       "\n",
       "   page_dc127d306179477fef4f3a9378dc550b_count  \\\n",
       "0                                         21.0   \n",
       "1                                         48.0   \n",
       "2                                         77.0   \n",
       "3                                         35.0   \n",
       "4                                        103.0   \n",
       "\n",
       "   page_dc127d306179477fef4f3a9378dc550b_visit_days  \\\n",
       "0                                              12.0   \n",
       "1                                              23.0   \n",
       "2                                              26.0   \n",
       "3                                              21.0   \n",
       "4                                              32.0   \n",
       "\n",
       "   page_dc127d306179477fef4f3a9378dc550b_last_visit  \\\n",
       "0                                              66.0   \n",
       "1                                               0.0   \n",
       "2                                               0.0   \n",
       "3                                               4.0   \n",
       "4                                               1.0   \n",
       "\n",
       "   page_a3efea933884689e89b46cadd9aa989e_count  \\\n",
       "0                                         16.0   \n",
       "1                                          9.0   \n",
       "2                                          NaN   \n",
       "3                                          NaN   \n",
       "4                                          3.0   \n",
       "\n",
       "   page_a3efea933884689e89b46cadd9aa989e_visit_days  \\\n",
       "0                                              12.0   \n",
       "1                                               2.0   \n",
       "2                                               NaN   \n",
       "3                                               NaN   \n",
       "4                                               2.0   \n",
       "\n",
       "   page_a3efea933884689e89b46cadd9aa989e_last_visit  \\\n",
       "0                                              66.0   \n",
       "1                                              67.0   \n",
       "2                                               NaN   \n",
       "3                                               NaN   \n",
       "4                                              18.0   \n",
       "\n",
       "   page_796f39eb9f0d6e6ff63d327d96fedeb5_count  \\\n",
       "0                                          NaN   \n",
       "1                                         33.0   \n",
       "2                                         39.0   \n",
       "3                                          NaN   \n",
       "4                                         46.0   \n",
       "\n",
       "   page_796f39eb9f0d6e6ff63d327d96fedeb5_visit_days  \\\n",
       "0                                               NaN   \n",
       "1                                              22.0   \n",
       "2                                              25.0   \n",
       "3                                               NaN   \n",
       "4                                              32.0   \n",
       "\n",
       "   page_796f39eb9f0d6e6ff63d327d96fedeb5_last_visit  ...  \\\n",
       "0                                               NaN  ...   \n",
       "1                                               0.0  ...   \n",
       "2                                               0.0  ...   \n",
       "3                                               NaN  ...   \n",
       "4                                               1.0  ...   \n",
       "\n",
       "   module_d166e7d89059242e8210b0b647206cb1_last_visit  \\\n",
       "0                                                NaN    \n",
       "1                                                NaN    \n",
       "2                                                NaN    \n",
       "3                                                NaN    \n",
       "4                                                NaN    \n",
       "\n",
       "   module_2613588ce2fcc962c5511a4e3eb234ba_count  \\\n",
       "0                                            NaN   \n",
       "1                                            NaN   \n",
       "2                                            NaN   \n",
       "3                                            NaN   \n",
       "4                                            NaN   \n",
       "\n",
       "   module_2613588ce2fcc962c5511a4e3eb234ba_unique_pages  \\\n",
       "0                                                NaN      \n",
       "1                                                NaN      \n",
       "2                                                NaN      \n",
       "3                                                NaN      \n",
       "4                                                NaN      \n",
       "\n",
       "   module_2613588ce2fcc962c5511a4e3eb234ba_last_visit  \\\n",
       "0                                                NaN    \n",
       "1                                                NaN    \n",
       "2                                                NaN    \n",
       "3                                                NaN    \n",
       "4                                                NaN    \n",
       "\n",
       "   module_1993162004f0f20fe0bcda9337e150b4_count  \\\n",
       "0                                            NaN   \n",
       "1                                            NaN   \n",
       "2                                            NaN   \n",
       "3                                            NaN   \n",
       "4                                            NaN   \n",
       "\n",
       "   module_1993162004f0f20fe0bcda9337e150b4_unique_pages  \\\n",
       "0                                                NaN      \n",
       "1                                                NaN      \n",
       "2                                                NaN      \n",
       "3                                                NaN      \n",
       "4                                                NaN      \n",
       "\n",
       "   module_1993162004f0f20fe0bcda9337e150b4_last_visit  \\\n",
       "0                                                NaN    \n",
       "1                                                NaN    \n",
       "2                                                NaN    \n",
       "3                                                NaN    \n",
       "4                                                NaN    \n",
       "\n",
       "   module_37d09e3760ce813af229ff907157fedf_count  \\\n",
       "0                                            NaN   \n",
       "1                                            NaN   \n",
       "2                                            NaN   \n",
       "3                                            NaN   \n",
       "4                                            NaN   \n",
       "\n",
       "   module_37d09e3760ce813af229ff907157fedf_unique_pages  \\\n",
       "0                                                NaN      \n",
       "1                                                NaN      \n",
       "2                                                NaN      \n",
       "3                                                NaN      \n",
       "4                                                NaN      \n",
       "\n",
       "   module_37d09e3760ce813af229ff907157fedf_last_visit  \n",
       "0                                                NaN   \n",
       "1                                                NaN   \n",
       "2                                                NaN   \n",
       "3                                                NaN   \n",
       "4                                                NaN   \n",
       "\n",
       "[5 rows x 301 columns]"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 4. 页面/模块分组统计特征\n",
    "def gen_mb_page_module_group_features(df):\n",
    "    \"\"\"\n",
    "    对每个页面和模块进行分组统计\n",
    "    包括: 访问次数、访问时间分布、访问频率等\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 4.1 每个页面的访问统计\n",
    "    print(\"生成页面访问统计特征...\")\n",
    "    \n",
    "    # 获取Top页面\n",
    "    top_pages = df['PAGE_TITLE'].value_counts().head(80).index.tolist()\n",
    "    \n",
    "    # 对Top页面进行多维度统计\n",
    "    for page in tqdm(top_pages[:60], desc='页面分组特征'):  # 控制在60个以内\n",
    "        df_page = df[df['PAGE_TITLE'] == page]\n",
    "        \n",
    "        # 访问次数\n",
    "        tmp = df_page.groupby('CUST_NO').size().to_frame(f'page_{page}_count').reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 访问天数\n",
    "        tmp = df_page.groupby('CUST_NO')['date_days_to_now'].nunique().to_frame(\n",
    "            f'page_{page}_visit_days'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 最近访问距今天数\n",
    "        tmp = df_page.groupby('CUST_NO')['date_days_to_now'].min().to_frame(\n",
    "            f'page_{page}_last_visit'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 4.2 每个模块的访问统计\n",
    "    print(\"生成模块访问统计特征...\")\n",
    "    \n",
    "    # 获取Top模块\n",
    "    top_modules = df['MODEL_NAME'].value_counts().head(50).index.tolist()\n",
    "    \n",
    "    # 对Top模块进行多维度统计\n",
    "    for module in tqdm(top_modules[:40], desc='模块分组特征'): \n",
    "        df_module = df[df['MODEL_NAME'] == module]\n",
    "        \n",
    "        # 访问次数\n",
    "        tmp = df_module.groupby('CUST_NO').size().to_frame(f'module_{module}_count').reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 访问的不同页面数\n",
    "        tmp = df_module.groupby('CUST_NO')['PAGE_TITLE'].nunique().to_frame(\n",
    "            f'module_{module}_unique_pages'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 最近访问距今天数\n",
    "        tmp = df_module.groupby('CUST_NO')['date_days_to_now'].min().to_frame(\n",
    "            f'module_{module}_last_visit'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_page_module_features = gen_mb_page_module_group_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 页面/模块分组特征数量: {mb_page_module_features.shape[1] - 1}\")\n",
    "mb_page_module_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "95dcdd38",
   "metadata": {},
   "source": [
    "## 时间段访问行为特征函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "7685b1e0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 时间段行为特征数量: 28\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>month_0_visit_count</th>\n",
       "      <th>month_0_page_nunique</th>\n",
       "      <th>month_0_module_nunique</th>\n",
       "      <th>month_0_active_days</th>\n",
       "      <th>month_1_visit_count</th>\n",
       "      <th>month_1_page_nunique</th>\n",
       "      <th>month_1_module_nunique</th>\n",
       "      <th>month_1_active_days</th>\n",
       "      <th>month_2_visit_count</th>\n",
       "      <th>...</th>\n",
       "      <th>weekday_weekend_ratio</th>\n",
       "      <th>month_start_visit_count</th>\n",
       "      <th>month_end_visit_count</th>\n",
       "      <th>dayofweek_0_count</th>\n",
       "      <th>dayofweek_1_count</th>\n",
       "      <th>dayofweek_2_count</th>\n",
       "      <th>dayofweek_3_count</th>\n",
       "      <th>dayofweek_4_count</th>\n",
       "      <th>dayofweek_5_count</th>\n",
       "      <th>dayofweek_6_count</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>56.0</td>\n",
       "      <td>...</td>\n",
       "      <td>10.400000</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>7.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>13.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>58.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>36.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>39.0</td>\n",
       "      <td>...</td>\n",
       "      <td>2.828571</td>\n",
       "      <td>4.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>34.0</td>\n",
       "      <td>26.0</td>\n",
       "      <td>20.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>30.0</td>\n",
       "      <td>4.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>104.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>36.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>55.0</td>\n",
       "      <td>...</td>\n",
       "      <td>2.920000</td>\n",
       "      <td>8.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>37.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>29.0</td>\n",
       "      <td>32.0</td>\n",
       "      <td>37.0</td>\n",
       "      <td>25.0</td>\n",
       "      <td>24.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>46.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>34.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>59.0</td>\n",
       "      <td>...</td>\n",
       "      <td>6.368421</td>\n",
       "      <td>4.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>26.0</td>\n",
       "      <td>38.0</td>\n",
       "      <td>30.0</td>\n",
       "      <td>19.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>11.0</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>96.0</td>\n",
       "      <td>17.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>120.0</td>\n",
       "      <td>24.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>115.0</td>\n",
       "      <td>...</td>\n",
       "      <td>5.148148</td>\n",
       "      <td>11.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>20.0</td>\n",
       "      <td>40.0</td>\n",
       "      <td>93.0</td>\n",
       "      <td>62.0</td>\n",
       "      <td>63.0</td>\n",
       "      <td>40.0</td>\n",
       "      <td>13.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 29 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  month_0_visit_count  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278                  NaN   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7                 58.0   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c                104.0   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f                 46.0   \n",
       "4  3f60ef34ad3853819e4269469280177d                 96.0   \n",
       "\n",
       "   month_0_page_nunique  month_0_module_nunique  month_0_active_days  \\\n",
       "0                   NaN                     NaN                  NaN   \n",
       "1                   5.0                     5.0                 11.0   \n",
       "2                  15.0                     6.0                 12.0   \n",
       "3                   7.0                     4.0                  8.0   \n",
       "4                  17.0                     6.0                 12.0   \n",
       "\n",
       "   month_1_visit_count  month_1_page_nunique  month_1_module_nunique  \\\n",
       "0                  NaN                   NaN                     NaN   \n",
       "1                 36.0                   5.0                     4.0   \n",
       "2                 36.0                   7.0                     4.0   \n",
       "3                 34.0                   6.0                     3.0   \n",
       "4                120.0                  24.0                     8.0   \n",
       "\n",
       "   month_1_active_days  month_2_visit_count  ...  weekday_weekend_ratio  \\\n",
       "0                  NaN                 56.0  ...              10.400000   \n",
       "1                  6.0                 39.0  ...               2.828571   \n",
       "2                  6.0                 55.0  ...               2.920000   \n",
       "3                  6.0                 59.0  ...               6.368421   \n",
       "4                 12.0                115.0  ...               5.148148   \n",
       "\n",
       "   month_start_visit_count  month_end_visit_count  dayofweek_0_count  \\\n",
       "0                      NaN                    NaN                7.0   \n",
       "1                      4.0                   11.0               34.0   \n",
       "2                      8.0                    5.0               37.0   \n",
       "3                      4.0                    NaN               26.0   \n",
       "4                     11.0                    NaN               20.0   \n",
       "\n",
       "   dayofweek_1_count  dayofweek_2_count  dayofweek_3_count  dayofweek_4_count  \\\n",
       "0               15.0               11.0               13.0                6.0   \n",
       "1               26.0               20.0               15.0                4.0   \n",
       "2               11.0               29.0               32.0               37.0   \n",
       "3               38.0               30.0               19.0                8.0   \n",
       "4               40.0               93.0               62.0               63.0   \n",
       "\n",
       "   dayofweek_5_count  dayofweek_6_count  \n",
       "0                4.0                NaN  \n",
       "1               30.0                4.0  \n",
       "2               25.0               24.0  \n",
       "3                7.0               11.0  \n",
       "4               40.0               13.0  \n",
       "\n",
       "[5 rows x 29 columns]"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 5. 时间段访问行为特征\n",
    "def gen_mb_time_period_features(df):\n",
    "    \"\"\"\n",
    "    按不同时间段(月/周/工作日/周末等)统计访问行为\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 5.1 按月份统计\n",
    "    for month in [0, 1, 2]:\n",
    "        df_month = df[df['date_months_to_now'] == month]\n",
    "        \n",
    "        # 该月访问次数\n",
    "        tmp = df_month.groupby('CUST_NO').size().to_frame(f'month_{month}_visit_count').reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 该月访问页面数\n",
    "        tmp = df_month.groupby('CUST_NO')['PAGE_TITLE'].nunique().to_frame(\n",
    "            f'month_{month}_page_nunique'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 该月访问模块数\n",
    "        tmp = df_month.groupby('CUST_NO')['MODEL_NAME'].nunique().to_frame(\n",
    "            f'month_{month}_module_nunique'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "        \n",
    "        # 该月访问天数\n",
    "        tmp = df_month.groupby('CUST_NO')['date_days_to_now'].nunique().to_frame(\n",
    "            f'month_{month}_active_days'\n",
    "        ).reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 5.2 工作日 vs 周末\n",
    "    df_weekday = df[df['operation_is_weekend'] == 0]\n",
    "    df_weekend = df[df['operation_is_weekend'] == 1]\n",
    "    \n",
    "    # 工作日访问统计\n",
    "    tmp = df_weekday.groupby('CUST_NO').agg({\n",
    "        'PAGE_TITLE': ['count', 'nunique'],\n",
    "        'MODEL_NAME': 'nunique'\n",
    "    })\n",
    "    tmp.columns = ['weekday_visit_count', 'weekday_page_nunique', 'weekday_module_nunique']\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 周末访问统计\n",
    "    tmp = df_weekend.groupby('CUST_NO').agg({\n",
    "        'PAGE_TITLE': ['count', 'nunique'],\n",
    "        'MODEL_NAME': 'nunique'\n",
    "    })\n",
    "    tmp.columns = ['weekend_visit_count', 'weekend_page_nunique', 'weekend_module_nunique']\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 工作日/周末访问比例\n",
    "    feature['weekday_weekend_ratio'] = feature['weekday_visit_count'] / (feature['weekend_visit_count'] + 1)\n",
    "    \n",
    "    # 5.3 月初/月末行为\n",
    "    df_month_start = df[df['operation_is_month_start'] == 1]\n",
    "    df_month_end = df[df['operation_is_month_end'] == 1]\n",
    "    \n",
    "    # 月初访问次数\n",
    "    tmp = df_month_start.groupby('CUST_NO').size().to_frame('month_start_visit_count').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 月末访问次数\n",
    "    tmp = df_month_end.groupby('CUST_NO').size().to_frame('month_end_visit_count').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 5.4 按周几统计\n",
    "    for dayofweek in range(7):\n",
    "        df_day = df[df['operation_dayofweek'] == dayofweek]\n",
    "        tmp = df_day.groupby('CUST_NO').size().to_frame(f'dayofweek_{dayofweek}_count').reset_index()\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_time_period_features = gen_mb_time_period_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 时间段行为特征数量: {mb_time_period_features.shape[1] - 1}\")\n",
    "mb_time_period_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "374590b2",
   "metadata": {},
   "source": [
    "## 用户访问习惯特征函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "bc641464",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 用户习惯特征数量: 24\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>total_visit_count</th>\n",
       "      <th>total_page_nunique</th>\n",
       "      <th>total_module_nunique</th>\n",
       "      <th>total_referrer_nunique</th>\n",
       "      <th>visit_days_min</th>\n",
       "      <th>visit_days_max</th>\n",
       "      <th>visit_days_mean</th>\n",
       "      <th>visit_days_std</th>\n",
       "      <th>visit_days_span</th>\n",
       "      <th>...</th>\n",
       "      <th>daily_visits_std</th>\n",
       "      <th>daily_visits_skew</th>\n",
       "      <th>daily_visits_kurt</th>\n",
       "      <th>top1_page_ratio</th>\n",
       "      <th>module_switch_count</th>\n",
       "      <th>module_switch_ratio</th>\n",
       "      <th>active_days</th>\n",
       "      <th>active_days_ratio</th>\n",
       "      <th>first_visit_days</th>\n",
       "      <th>last_visit_days</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>56</td>\n",
       "      <td>6</td>\n",
       "      <td>3</td>\n",
       "      <td>7</td>\n",
       "      <td>66</td>\n",
       "      <td>89</td>\n",
       "      <td>76.982143</td>\n",
       "      <td>6.900569</td>\n",
       "      <td>23</td>\n",
       "      <td>...</td>\n",
       "      <td>2.774341</td>\n",
       "      <td>2.069584</td>\n",
       "      <td>4.120448</td>\n",
       "      <td>0.375000</td>\n",
       "      <td>5</td>\n",
       "      <td>0.089286</td>\n",
       "      <td>12</td>\n",
       "      <td>0.500000</td>\n",
       "      <td>66</td>\n",
       "      <td>89</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>133</td>\n",
       "      <td>11</td>\n",
       "      <td>5</td>\n",
       "      <td>12</td>\n",
       "      <td>0</td>\n",
       "      <td>90</td>\n",
       "      <td>42.105263</td>\n",
       "      <td>28.971842</td>\n",
       "      <td>90</td>\n",
       "      <td>...</td>\n",
       "      <td>3.044466</td>\n",
       "      <td>1.507036</td>\n",
       "      <td>1.575973</td>\n",
       "      <td>0.360902</td>\n",
       "      <td>91</td>\n",
       "      <td>0.684211</td>\n",
       "      <td>23</td>\n",
       "      <td>0.252747</td>\n",
       "      <td>0</td>\n",
       "      <td>90</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>195</td>\n",
       "      <td>18</td>\n",
       "      <td>7</td>\n",
       "      <td>16</td>\n",
       "      <td>0</td>\n",
       "      <td>88</td>\n",
       "      <td>37.794872</td>\n",
       "      <td>31.508287</td>\n",
       "      <td>88</td>\n",
       "      <td>...</td>\n",
       "      <td>3.569314</td>\n",
       "      <td>1.606666</td>\n",
       "      <td>3.136583</td>\n",
       "      <td>0.394872</td>\n",
       "      <td>137</td>\n",
       "      <td>0.702564</td>\n",
       "      <td>26</td>\n",
       "      <td>0.292135</td>\n",
       "      <td>0</td>\n",
       "      <td>88</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>139</td>\n",
       "      <td>13</td>\n",
       "      <td>7</td>\n",
       "      <td>14</td>\n",
       "      <td>4</td>\n",
       "      <td>90</td>\n",
       "      <td>48.287770</td>\n",
       "      <td>26.873899</td>\n",
       "      <td>86</td>\n",
       "      <td>...</td>\n",
       "      <td>5.103687</td>\n",
       "      <td>2.237539</td>\n",
       "      <td>5.979316</td>\n",
       "      <td>0.302158</td>\n",
       "      <td>80</td>\n",
       "      <td>0.575540</td>\n",
       "      <td>21</td>\n",
       "      <td>0.241379</td>\n",
       "      <td>4</td>\n",
       "      <td>90</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>331</td>\n",
       "      <td>38</td>\n",
       "      <td>11</td>\n",
       "      <td>36</td>\n",
       "      <td>1</td>\n",
       "      <td>90</td>\n",
       "      <td>50.277946</td>\n",
       "      <td>24.105729</td>\n",
       "      <td>89</td>\n",
       "      <td>...</td>\n",
       "      <td>8.334187</td>\n",
       "      <td>1.518758</td>\n",
       "      <td>1.176220</td>\n",
       "      <td>0.311178</td>\n",
       "      <td>247</td>\n",
       "      <td>0.746224</td>\n",
       "      <td>32</td>\n",
       "      <td>0.355556</td>\n",
       "      <td>1</td>\n",
       "      <td>90</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 25 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  total_visit_count  total_page_nunique  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278                 56                   6   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7                133                  11   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c                195                  18   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f                139                  13   \n",
       "4  3f60ef34ad3853819e4269469280177d                331                  38   \n",
       "\n",
       "   total_module_nunique  total_referrer_nunique  visit_days_min  \\\n",
       "0                     3                       7              66   \n",
       "1                     5                      12               0   \n",
       "2                     7                      16               0   \n",
       "3                     7                      14               4   \n",
       "4                    11                      36               1   \n",
       "\n",
       "   visit_days_max  visit_days_mean  visit_days_std  visit_days_span  ...  \\\n",
       "0              89        76.982143        6.900569               23  ...   \n",
       "1              90        42.105263       28.971842               90  ...   \n",
       "2              88        37.794872       31.508287               88  ...   \n",
       "3              90        48.287770       26.873899               86  ...   \n",
       "4              90        50.277946       24.105729               89  ...   \n",
       "\n",
       "   daily_visits_std  daily_visits_skew  daily_visits_kurt  top1_page_ratio  \\\n",
       "0          2.774341           2.069584           4.120448         0.375000   \n",
       "1          3.044466           1.507036           1.575973         0.360902   \n",
       "2          3.569314           1.606666           3.136583         0.394872   \n",
       "3          5.103687           2.237539           5.979316         0.302158   \n",
       "4          8.334187           1.518758           1.176220         0.311178   \n",
       "\n",
       "   module_switch_count  module_switch_ratio  active_days  active_days_ratio  \\\n",
       "0                    5             0.089286           12           0.500000   \n",
       "1                   91             0.684211           23           0.252747   \n",
       "2                  137             0.702564           26           0.292135   \n",
       "3                   80             0.575540           21           0.241379   \n",
       "4                  247             0.746224           32           0.355556   \n",
       "\n",
       "   first_visit_days  last_visit_days  \n",
       "0                66               89  \n",
       "1                 0               90  \n",
       "2                 0               88  \n",
       "3                 4               90  \n",
       "4                 1               90  \n",
       "\n",
       "[5 rows x 25 columns]"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 6. 用户访问习惯特征\n",
    "def gen_mb_user_habit_features(df):\n",
    "    \"\"\"\n",
    "    挖掘用户的访问习惯和偏好特征\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 6.1 整体访问统计\n",
    "    tmp = df.groupby('CUST_NO').agg({\n",
    "        'PAGE_TITLE': ['count', 'nunique'],\n",
    "        'MODEL_NAME': 'nunique',\n",
    "        'REFERRER_TITLE': 'nunique',\n",
    "        'date_days_to_now': ['min', 'max', 'mean', 'std']\n",
    "    })\n",
    "    tmp.columns = [\n",
    "        'total_visit_count', 'total_page_nunique', 'total_module_nunique', \n",
    "        'total_referrer_nunique', 'visit_days_min', 'visit_days_max', \n",
    "        'visit_days_mean', 'visit_days_std'\n",
    "    ]\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 访问跨度\n",
    "    feature['visit_days_span'] = feature['visit_days_max'] - feature['visit_days_min']\n",
    "    \n",
    "    # 访问集中度 (访问越集中,std越小)\n",
    "    feature['visit_concentration'] = 1 / (feature['visit_days_std'] + 1)\n",
    "    \n",
    "    # 平均每天访问的页面数\n",
    "    feature['avg_pages_per_day'] = feature['total_page_nunique'] / (feature['visit_days_span'] + 1)\n",
    "    \n",
    "    # 6.2 访问频率特征\n",
    "    # 每个用户每天的访问次数\n",
    "    df_daily_visits = df.groupby(['CUST_NO', 'date_days_to_now']).size().reset_index(name='daily_visits')\n",
    "    \n",
    "    tmp = df_daily_visits.groupby('CUST_NO')['daily_visits'].agg([\n",
    "        'mean', 'max', 'min', 'std', \n",
    "        ('skew', lambda x: x.skew()),\n",
    "        ('kurt', lambda x: x.kurt())\n",
    "    ])\n",
    "    tmp.columns = [f'daily_visits_{col}' for col in tmp.columns]\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 6.3 页面浏览深度特征\n",
    "    # 用户最常访问的页面占比\n",
    "    df_page_freq = df.groupby(['CUST_NO', 'PAGE_TITLE']).size().reset_index(name='page_count')\n",
    "    df_top1_page = df_page_freq.sort_values(['CUST_NO', 'page_count'], ascending=[True, False])\n",
    "    df_top1_page = df_top1_page.groupby('CUST_NO').first().reset_index()\n",
    "    df_top1_page['top1_page_ratio'] = df_top1_page['page_count'] / df_page_freq.groupby('CUST_NO')['page_count'].sum().values\n",
    "    feature = feature.merge(df_top1_page[['CUST_NO', 'top1_page_ratio']], on='CUST_NO', how='left')\n",
    "    \n",
    "    # 6.4 模块切换频率\n",
    "    # 计算用户在不同模块之间的跳转次数\n",
    "    df_sorted = df.sort_values(['CUST_NO', 'OPERATION_DATE', 'date_days_to_now'])\n",
    "    df_sorted['next_module'] = df_sorted.groupby('CUST_NO')['MODEL_NAME'].shift(-1)\n",
    "    df_sorted['module_switch'] = (df_sorted['MODEL_NAME'] != df_sorted['next_module']).astype(int)\n",
    "    \n",
    "    tmp = df_sorted.groupby('CUST_NO')['module_switch'].agg(['sum', 'mean'])\n",
    "    tmp.columns = ['module_switch_count', 'module_switch_ratio']\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 6.5 访问规律性特征\n",
    "    # 访问天数占总天数的比例\n",
    "    tmp = df.groupby('CUST_NO')['date_days_to_now'].nunique().to_frame('active_days').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    feature['active_days_ratio'] = feature['active_days'] / (feature['visit_days_span'] + 1)\n",
    "    \n",
    "    # 6.6 首次和末次访问特征\n",
    "    tmp = df.groupby('CUST_NO').agg({\n",
    "        'date_days_to_now': ['min', 'max'],\n",
    "        'PAGE_TITLE': 'first',\n",
    "        'MODEL_NAME': 'last'\n",
    "    })\n",
    "    tmp.columns = ['first_visit_days', 'last_visit_days', 'first_page', 'last_module']\n",
    "    tmp = tmp.reset_index()\n",
    "    \n",
    "    # 首次访问距今天数\n",
    "    feature = feature.merge(tmp[['CUST_NO', 'first_visit_days', 'last_visit_days']], on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_user_habit_features = gen_mb_user_habit_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 用户习惯特征数量: {mb_user_habit_features.shape[1] - 1}\")\n",
    "mb_user_habit_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e6e2ece3",
   "metadata": {},
   "source": [
    "## 趋势变化特征"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "c817a1e7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 趋势变化特征数量: 33\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>m0_visit_count</th>\n",
       "      <th>m0_page_nunique</th>\n",
       "      <th>m0_module_nunique</th>\n",
       "      <th>m1_visit_count</th>\n",
       "      <th>m1_page_nunique</th>\n",
       "      <th>m1_module_nunique</th>\n",
       "      <th>m2_visit_count</th>\n",
       "      <th>m2_page_nunique</th>\n",
       "      <th>m2_module_nunique</th>\n",
       "      <th>...</th>\n",
       "      <th>week_5_count</th>\n",
       "      <th>week_6_count</th>\n",
       "      <th>week_7_count</th>\n",
       "      <th>week_8_count</th>\n",
       "      <th>week_9_count</th>\n",
       "      <th>week_10_count</th>\n",
       "      <th>week_11_count</th>\n",
       "      <th>recent_4weeks_std</th>\n",
       "      <th>recent_4weeks_mean</th>\n",
       "      <th>recent_4weeks_cv</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>56.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>7.0</td>\n",
       "      <td>29.0</td>\n",
       "      <td>9.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>58.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>36.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>39.0</td>\n",
       "      <td>9.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>...</td>\n",
       "      <td>10.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>14.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>4.0</td>\n",
       "      <td>4.932883</td>\n",
       "      <td>17.333333</td>\n",
       "      <td>0.269066</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>104.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>36.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>55.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>...</td>\n",
       "      <td>NaN</td>\n",
       "      <td>10.0</td>\n",
       "      <td>13.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>12.0</td>\n",
       "      <td>18.0</td>\n",
       "      <td>19.096247</td>\n",
       "      <td>26.000000</td>\n",
       "      <td>0.707268</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>46.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>34.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>3.0</td>\n",
       "      <td>59.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>4.0</td>\n",
       "      <td>...</td>\n",
       "      <td>3.0</td>\n",
       "      <td>16.0</td>\n",
       "      <td>12.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>24.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>20.0</td>\n",
       "      <td>5.744563</td>\n",
       "      <td>11.500000</td>\n",
       "      <td>0.459565</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>96.0</td>\n",
       "      <td>17.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>120.0</td>\n",
       "      <td>24.0</td>\n",
       "      <td>8.0</td>\n",
       "      <td>115.0</td>\n",
       "      <td>18.0</td>\n",
       "      <td>7.0</td>\n",
       "      <td>...</td>\n",
       "      <td>27.0</td>\n",
       "      <td>35.0</td>\n",
       "      <td>30.0</td>\n",
       "      <td>20.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>39.0</td>\n",
       "      <td>47.0</td>\n",
       "      <td>21.654484</td>\n",
       "      <td>22.750000</td>\n",
       "      <td>0.911768</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 34 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  m0_visit_count  m0_page_nunique  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278             NaN              NaN   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7            58.0              5.0   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c           104.0             15.0   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f            46.0              7.0   \n",
       "4  3f60ef34ad3853819e4269469280177d            96.0             17.0   \n",
       "\n",
       "   m0_module_nunique  m1_visit_count  m1_page_nunique  m1_module_nunique  \\\n",
       "0                NaN             NaN              NaN                NaN   \n",
       "1                5.0            36.0              5.0                4.0   \n",
       "2                6.0            36.0              7.0                4.0   \n",
       "3                4.0            34.0              6.0                3.0   \n",
       "4                6.0           120.0             24.0                8.0   \n",
       "\n",
       "   m2_visit_count  m2_page_nunique  m2_module_nunique  ...  week_5_count  \\\n",
       "0            56.0              6.0                3.0  ...           NaN   \n",
       "1            39.0              9.0                4.0  ...          10.0   \n",
       "2            55.0              6.0                4.0  ...           NaN   \n",
       "3            59.0              7.0                4.0  ...           3.0   \n",
       "4           115.0             18.0                7.0  ...          27.0   \n",
       "\n",
       "   week_6_count  week_7_count  week_8_count  week_9_count  week_10_count  \\\n",
       "0           NaN           NaN           NaN           7.0           29.0   \n",
       "1           4.0           8.0          14.0          15.0            NaN   \n",
       "2          10.0          13.0           8.0           NaN           12.0   \n",
       "3          16.0          12.0           NaN          24.0            7.0   \n",
       "4          35.0          30.0          20.0          11.0           39.0   \n",
       "\n",
       "   week_11_count  recent_4weeks_std  recent_4weeks_mean  recent_4weeks_cv  \n",
       "0            9.0                NaN                 NaN               NaN  \n",
       "1            4.0           4.932883           17.333333          0.269066  \n",
       "2           18.0          19.096247           26.000000          0.707268  \n",
       "3           20.0           5.744563           11.500000          0.459565  \n",
       "4           47.0          21.654484           22.750000          0.911768  \n",
       "\n",
       "[5 rows x 34 columns]"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 7. 趋势变化特征\n",
    "def gen_mb_trend_features(df):\n",
    "    \"\"\"\n",
    "    捕捉用户访问行为的趋势变化\n",
    "    比较不同月份之间的变化\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 7.1 月度访问趋势\n",
    "    # 第0月(最近)、第1月(次近)、第2月(最早)的访问统计\n",
    "    month_stats = {}\n",
    "    for month in [0, 1, 2]:\n",
    "        df_month = df[df['date_months_to_now'] == month]\n",
    "        tmp = df_month.groupby('CUST_NO').agg({\n",
    "            'PAGE_TITLE': ['count', 'nunique'],\n",
    "            'MODEL_NAME': 'nunique'\n",
    "        })\n",
    "        tmp.columns = [f'm{month}_visit_count', f'm{month}_page_nunique', f'm{month}_module_nunique']\n",
    "        month_stats[month] = tmp.reset_index()\n",
    "    \n",
    "    # 合并月度统计\n",
    "    for month in [0, 1, 2]:\n",
    "        feature = feature.merge(month_stats[month], on='CUST_NO', how='left')\n",
    "    \n",
    "    # 7.2 月度变化率\n",
    "    # 最近月相对次近月的变化\n",
    "    feature['visit_count_change_m0_m1'] = (feature['m0_visit_count'] - feature['m1_visit_count']) / (feature['m1_visit_count'] + 1)\n",
    "    feature['page_nunique_change_m0_m1'] = (feature['m0_page_nunique'] - feature['m1_page_nunique']) / (feature['m1_page_nunique'] + 1)\n",
    "    \n",
    "    # 次近月相对最早月的变化\n",
    "    feature['visit_count_change_m1_m2'] = (feature['m1_visit_count'] - feature['m2_visit_count']) / (feature['m2_visit_count'] + 1)\n",
    "    feature['page_nunique_change_m1_m2'] = (feature['m1_page_nunique'] - feature['m2_page_nunique']) / (feature['m2_page_nunique'] + 1)\n",
    "    \n",
    "    # 最近月相对最早月的变化\n",
    "    feature['visit_count_change_m0_m2'] = (feature['m0_visit_count'] - feature['m2_visit_count']) / (feature['m2_visit_count'] + 1)\n",
    "    feature['page_nunique_change_m0_m2'] = (feature['m0_page_nunique'] - feature['m2_page_nunique']) / (feature['m2_page_nunique'] + 1)\n",
    "    \n",
    "    # 7.3 访问加速度(二阶差分)\n",
    "    feature['visit_acceleration'] = feature['visit_count_change_m0_m1'] - feature['visit_count_change_m1_m2']\n",
    "    \n",
    "    # 7.4 访问趋势(上升/下降/稳定)\n",
    "    feature['visit_trend_up'] = ((feature['m0_visit_count'] > feature['m1_visit_count']) & \n",
    "                                  (feature['m1_visit_count'] > feature['m2_visit_count'])).astype(int)\n",
    "    feature['visit_trend_down'] = ((feature['m0_visit_count'] < feature['m1_visit_count']) & \n",
    "                                    (feature['m1_visit_count'] < feature['m2_visit_count'])).astype(int)\n",
    "    \n",
    "    # 7.5 周度访问变化\n",
    "    week_stats = {}\n",
    "    for week in range(min(df['date_weeks_to_now'].max() + 1, 12)):  # 最多12周\n",
    "        df_week = df[df['date_weeks_to_now'] == week]\n",
    "        tmp = df_week.groupby('CUST_NO').size().to_frame(f'week_{week}_count').reset_index()\n",
    "        week_stats[week] = tmp\n",
    "        feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 计算最近4周的变化系数\n",
    "    if len(week_stats) >= 4:\n",
    "        week_cols = [f'week_{i}_count' for i in range(4)]\n",
    "        feature['recent_4weeks_std'] = feature[week_cols].std(axis=1)\n",
    "        feature['recent_4weeks_mean'] = feature[week_cols].mean(axis=1)\n",
    "        feature['recent_4weeks_cv'] = feature['recent_4weeks_std'] / (feature['recent_4weeks_mean'] + 1)\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_trend_features = gen_mb_trend_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 趋势变化特征数量: {mb_trend_features.shape[1] - 1}\")\n",
    "mb_trend_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bafbb6f4",
   "metadata": {},
   "source": [
    "## 交叉统计特征函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "e9c6b315",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 交叉统计特征数量: 17\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>module_avg_pages_mean</th>\n",
       "      <th>module_avg_pages_max</th>\n",
       "      <th>module_avg_pages_min</th>\n",
       "      <th>module_avg_pages_std</th>\n",
       "      <th>daily_unique_pages_mean</th>\n",
       "      <th>daily_unique_pages_max</th>\n",
       "      <th>daily_unique_pages_std</th>\n",
       "      <th>daily_unique_modules_mean</th>\n",
       "      <th>daily_unique_modules_max</th>\n",
       "      <th>daily_unique_modules_std</th>\n",
       "      <th>dayofweek_visit_std</th>\n",
       "      <th>dayofweek_visit_mean</th>\n",
       "      <th>dayofweek_visit_cv</th>\n",
       "      <th>m0_daily_module_diversity</th>\n",
       "      <th>m1_daily_module_diversity</th>\n",
       "      <th>m2_daily_module_diversity</th>\n",
       "      <th>top_module_ratio</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>2.000000</td>\n",
       "      <td>4</td>\n",
       "      <td>1</td>\n",
       "      <td>1.732051</td>\n",
       "      <td>3.250000</td>\n",
       "      <td>6</td>\n",
       "      <td>0.866025</td>\n",
       "      <td>1.166667</td>\n",
       "      <td>3</td>\n",
       "      <td>0.577350</td>\n",
       "      <td>4.320494</td>\n",
       "      <td>9.333333</td>\n",
       "      <td>0.418112</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>1.166667</td>\n",
       "      <td>0.964286</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>2.200000</td>\n",
       "      <td>7</td>\n",
       "      <td>1</td>\n",
       "      <td>2.683282</td>\n",
       "      <td>3.739130</td>\n",
       "      <td>7</td>\n",
       "      <td>1.053884</td>\n",
       "      <td>3.304348</td>\n",
       "      <td>4</td>\n",
       "      <td>0.702902</td>\n",
       "      <td>11.986103</td>\n",
       "      <td>19.000000</td>\n",
       "      <td>0.599305</td>\n",
       "      <td>3.454545</td>\n",
       "      <td>3.500000</td>\n",
       "      <td>2.833333</td>\n",
       "      <td>0.488722</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>2.571429</td>\n",
       "      <td>7</td>\n",
       "      <td>1</td>\n",
       "      <td>2.070197</td>\n",
       "      <td>4.500000</td>\n",
       "      <td>9</td>\n",
       "      <td>1.303840</td>\n",
       "      <td>3.461538</td>\n",
       "      <td>5</td>\n",
       "      <td>0.760567</td>\n",
       "      <td>9.063270</td>\n",
       "      <td>27.857143</td>\n",
       "      <td>0.314074</td>\n",
       "      <td>3.750000</td>\n",
       "      <td>2.833333</td>\n",
       "      <td>3.500000</td>\n",
       "      <td>0.528205</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>1.857143</td>\n",
       "      <td>4</td>\n",
       "      <td>1</td>\n",
       "      <td>1.069045</td>\n",
       "      <td>3.904762</td>\n",
       "      <td>7</td>\n",
       "      <td>1.179185</td>\n",
       "      <td>2.238095</td>\n",
       "      <td>4</td>\n",
       "      <td>0.624881</td>\n",
       "      <td>11.936339</td>\n",
       "      <td>19.857143</td>\n",
       "      <td>0.572290</td>\n",
       "      <td>2.250000</td>\n",
       "      <td>2.166667</td>\n",
       "      <td>2.285714</td>\n",
       "      <td>0.640288</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>3.454545</td>\n",
       "      <td>15</td>\n",
       "      <td>1</td>\n",
       "      <td>4.156047</td>\n",
       "      <td>6.125000</td>\n",
       "      <td>16</td>\n",
       "      <td>3.480360</td>\n",
       "      <td>4.250000</td>\n",
       "      <td>7</td>\n",
       "      <td>0.915811</td>\n",
       "      <td>27.626764</td>\n",
       "      <td>47.285714</td>\n",
       "      <td>0.572152</td>\n",
       "      <td>3.916667</td>\n",
       "      <td>4.083333</td>\n",
       "      <td>5.000000</td>\n",
       "      <td>0.471299</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  module_avg_pages_mean  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278               2.000000   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7               2.200000   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c               2.571429   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f               1.857143   \n",
       "4  3f60ef34ad3853819e4269469280177d               3.454545   \n",
       "\n",
       "   module_avg_pages_max  module_avg_pages_min  module_avg_pages_std  \\\n",
       "0                     4                     1              1.732051   \n",
       "1                     7                     1              2.683282   \n",
       "2                     7                     1              2.070197   \n",
       "3                     4                     1              1.069045   \n",
       "4                    15                     1              4.156047   \n",
       "\n",
       "   daily_unique_pages_mean  daily_unique_pages_max  daily_unique_pages_std  \\\n",
       "0                 3.250000                       6                0.866025   \n",
       "1                 3.739130                       7                1.053884   \n",
       "2                 4.500000                       9                1.303840   \n",
       "3                 3.904762                       7                1.179185   \n",
       "4                 6.125000                      16                3.480360   \n",
       "\n",
       "   daily_unique_modules_mean  daily_unique_modules_max  \\\n",
       "0                   1.166667                         3   \n",
       "1                   3.304348                         4   \n",
       "2                   3.461538                         5   \n",
       "3                   2.238095                         4   \n",
       "4                   4.250000                         7   \n",
       "\n",
       "   daily_unique_modules_std  dayofweek_visit_std  dayofweek_visit_mean  \\\n",
       "0                  0.577350             4.320494              9.333333   \n",
       "1                  0.702902            11.986103             19.000000   \n",
       "2                  0.760567             9.063270             27.857143   \n",
       "3                  0.624881            11.936339             19.857143   \n",
       "4                  0.915811            27.626764             47.285714   \n",
       "\n",
       "   dayofweek_visit_cv  m0_daily_module_diversity  m1_daily_module_diversity  \\\n",
       "0            0.418112                        NaN                        NaN   \n",
       "1            0.599305                   3.454545                   3.500000   \n",
       "2            0.314074                   3.750000                   2.833333   \n",
       "3            0.572290                   2.250000                   2.166667   \n",
       "4            0.572152                   3.916667                   4.083333   \n",
       "\n",
       "   m2_daily_module_diversity  top_module_ratio  \n",
       "0                   1.166667          0.964286  \n",
       "1                   2.833333          0.488722  \n",
       "2                   3.500000          0.528205  \n",
       "3                   2.285714          0.640288  \n",
       "4                   5.000000          0.471299  "
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 8. 交叉统计特征\n",
    "def gen_mb_cross_features(df):\n",
    "    \"\"\"\n",
    "    页面、模块、时间等多维度交叉统计\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 8.1 模块-页面交叉特征\n",
    "    # 每个模块下访问的平均页面数\n",
    "    tmp1 = df.groupby(['CUST_NO', 'MODEL_NAME'])['PAGE_TITLE'].nunique().reset_index()\n",
    "    tmp2 = tmp1.groupby('CUST_NO')['PAGE_TITLE'].agg(['mean', 'max', 'min', 'std'])\n",
    "    tmp2.columns = [f'module_avg_pages_{col}' for col in tmp2.columns]\n",
    "    tmp2 = tmp2.reset_index()\n",
    "    feature = feature.merge(tmp2, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 8.2 时间-页面交叉特征\n",
    "    # 每天访问的平均页面种类\n",
    "    tmp1 = df.groupby(['CUST_NO', 'date_days_to_now'])['PAGE_TITLE'].nunique().reset_index()\n",
    "    tmp2 = tmp1.groupby('CUST_NO')['PAGE_TITLE'].agg(['mean', 'max', 'std'])\n",
    "    tmp2.columns = [f'daily_unique_pages_{col}' for col in tmp2.columns]\n",
    "    tmp2 = tmp2.reset_index()\n",
    "    feature = feature.merge(tmp2, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 8.3 时间-模块交叉特征\n",
    "    # 每天访问的平均模块种类\n",
    "    tmp1 = df.groupby(['CUST_NO', 'date_days_to_now'])['MODEL_NAME'].nunique().reset_index()\n",
    "    tmp2 = tmp1.groupby('CUST_NO')['MODEL_NAME'].agg(['mean', 'max', 'std'])\n",
    "    tmp2.columns = [f'daily_unique_modules_{col}' for col in tmp2.columns]\n",
    "    tmp2 = tmp2.reset_index()\n",
    "    feature = feature.merge(tmp2, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 8.4 周几-访问量交叉\n",
    "    df_dayofweek_stats = df.groupby(['CUST_NO', 'operation_dayofweek']).size().reset_index(name='count')\n",
    "    tmp = df_dayofweek_stats.groupby('CUST_NO')['count'].agg(['std', 'mean'])\n",
    "    tmp.columns = ['dayofweek_visit_std', 'dayofweek_visit_mean']\n",
    "    tmp['dayofweek_visit_cv'] = tmp['dayofweek_visit_std'] / (tmp['dayofweek_visit_mean'] + 1)\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 8.5 月份-模块多样性\n",
    "    for month in [0, 1, 2]:\n",
    "        df_month = df[df['date_months_to_now'] == month]\n",
    "        # 该月每天访问的模块种类\n",
    "        tmp1 = df_month.groupby(['CUST_NO', 'date_days_to_now'])['MODEL_NAME'].nunique().reset_index()\n",
    "        tmp2 = tmp1.groupby('CUST_NO')['MODEL_NAME'].mean().to_frame(f'm{month}_daily_module_diversity').reset_index()\n",
    "        feature = feature.merge(tmp2, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 8.6 比例类交叉特征\n",
    "    # 最常访问的模块占比\n",
    "    df_module_freq = df.groupby(['CUST_NO', 'MODEL_NAME']).size().reset_index(name='module_count')\n",
    "    df_top_module = df_module_freq.sort_values(['CUST_NO', 'module_count'], ascending=[True, False])\n",
    "    df_top_module = df_top_module.groupby('CUST_NO').first().reset_index()\n",
    "    df_total_module = df.groupby('CUST_NO').size().reset_index(name='total_count')\n",
    "    df_top_module = df_top_module.merge(df_total_module, on='CUST_NO')\n",
    "    df_top_module['top_module_ratio'] = df_top_module['module_count'] / df_top_module['total_count']\n",
    "    feature = feature.merge(df_top_module[['CUST_NO', 'top_module_ratio']], on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_cross_features = gen_mb_cross_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 交叉统计特征数量: {mb_cross_features.shape[1] - 1}\")\n",
    "mb_cross_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "46e1c47e",
   "metadata": {},
   "source": [
    "## 序列行为特征函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "0c15cf53",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ 序列行为特征数量: 16\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>same_page_consecutive_count</th>\n",
       "      <th>same_page_consecutive_ratio</th>\n",
       "      <th>same_module_consecutive_count</th>\n",
       "      <th>same_module_consecutive_ratio</th>\n",
       "      <th>revisited_page_count</th>\n",
       "      <th>avg_page_visit_count</th>\n",
       "      <th>visit_interval_mean</th>\n",
       "      <th>visit_interval_max</th>\n",
       "      <th>visit_interval_min</th>\n",
       "      <th>visit_interval_std</th>\n",
       "      <th>total_sessions</th>\n",
       "      <th>session_visit_mean</th>\n",
       "      <th>session_visit_max</th>\n",
       "      <th>session_visit_std</th>\n",
       "      <th>recent_3_page_nunique</th>\n",
       "      <th>recent_3_module_nunique</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>864a14a62ffffbc4741d365ea5a08278</td>\n",
       "      <td>7</td>\n",
       "      <td>0.125000</td>\n",
       "      <td>51</td>\n",
       "      <td>0.910714</td>\n",
       "      <td>4.0</td>\n",
       "      <td>9.333333</td>\n",
       "      <td>0.418182</td>\n",
       "      <td>5.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.030838</td>\n",
       "      <td>6</td>\n",
       "      <td>8.000000</td>\n",
       "      <td>21</td>\n",
       "      <td>5.916080</td>\n",
       "      <td>3</td>\n",
       "      <td>1</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>74dfe9a67327540d1f427b40e85d49c7</td>\n",
       "      <td>28</td>\n",
       "      <td>0.210526</td>\n",
       "      <td>42</td>\n",
       "      <td>0.315789</td>\n",
       "      <td>8.0</td>\n",
       "      <td>12.090909</td>\n",
       "      <td>0.681818</td>\n",
       "      <td>11.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.982050</td>\n",
       "      <td>18</td>\n",
       "      <td>7.000000</td>\n",
       "      <td>17</td>\n",
       "      <td>4.163332</td>\n",
       "      <td>2</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>078f92ffdfdfe07bb958c10c01d0733c</td>\n",
       "      <td>33</td>\n",
       "      <td>0.169231</td>\n",
       "      <td>58</td>\n",
       "      <td>0.297436</td>\n",
       "      <td>9.0</td>\n",
       "      <td>10.833333</td>\n",
       "      <td>0.453608</td>\n",
       "      <td>13.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.682095</td>\n",
       "      <td>16</td>\n",
       "      <td>11.470588</td>\n",
       "      <td>40</td>\n",
       "      <td>10.648460</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>c1d381f50813111b4f1f8d3b124c369f</td>\n",
       "      <td>17</td>\n",
       "      <td>0.122302</td>\n",
       "      <td>59</td>\n",
       "      <td>0.424460</td>\n",
       "      <td>8.0</td>\n",
       "      <td>10.692308</td>\n",
       "      <td>0.623188</td>\n",
       "      <td>13.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.993430</td>\n",
       "      <td>13</td>\n",
       "      <td>9.928571</td>\n",
       "      <td>24</td>\n",
       "      <td>6.911131</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>3f60ef34ad3853819e4269469280177d</td>\n",
       "      <td>41</td>\n",
       "      <td>0.123867</td>\n",
       "      <td>84</td>\n",
       "      <td>0.253776</td>\n",
       "      <td>20.0</td>\n",
       "      <td>8.710526</td>\n",
       "      <td>0.269697</td>\n",
       "      <td>8.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.053287</td>\n",
       "      <td>21</td>\n",
       "      <td>15.045455</td>\n",
       "      <td>49</td>\n",
       "      <td>12.518471</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  same_page_consecutive_count  \\\n",
       "0  864a14a62ffffbc4741d365ea5a08278                            7   \n",
       "1  74dfe9a67327540d1f427b40e85d49c7                           28   \n",
       "2  078f92ffdfdfe07bb958c10c01d0733c                           33   \n",
       "3  c1d381f50813111b4f1f8d3b124c369f                           17   \n",
       "4  3f60ef34ad3853819e4269469280177d                           41   \n",
       "\n",
       "   same_page_consecutive_ratio  same_module_consecutive_count  \\\n",
       "0                     0.125000                             51   \n",
       "1                     0.210526                             42   \n",
       "2                     0.169231                             58   \n",
       "3                     0.122302                             59   \n",
       "4                     0.123867                             84   \n",
       "\n",
       "   same_module_consecutive_ratio  revisited_page_count  avg_page_visit_count  \\\n",
       "0                       0.910714                   4.0              9.333333   \n",
       "1                       0.315789                   8.0             12.090909   \n",
       "2                       0.297436                   9.0             10.833333   \n",
       "3                       0.424460                   8.0             10.692308   \n",
       "4                       0.253776                  20.0              8.710526   \n",
       "\n",
       "   visit_interval_mean  visit_interval_max  visit_interval_min  \\\n",
       "0             0.418182                 5.0                 0.0   \n",
       "1             0.681818                11.0                 0.0   \n",
       "2             0.453608                13.0                 0.0   \n",
       "3             0.623188                13.0                 0.0   \n",
       "4             0.269697                 8.0                 0.0   \n",
       "\n",
       "   visit_interval_std  total_sessions  session_visit_mean  session_visit_max  \\\n",
       "0            1.030838               6            8.000000                 21   \n",
       "1            1.982050              18            7.000000                 17   \n",
       "2            1.682095              16           11.470588                 40   \n",
       "3            1.993430              13            9.928571                 24   \n",
       "4            1.053287              21           15.045455                 49   \n",
       "\n",
       "   session_visit_std  recent_3_page_nunique  recent_3_module_nunique  \n",
       "0           5.916080                      3                        1  \n",
       "1           4.163332                      2                        2  \n",
       "2          10.648460                      3                        3  \n",
       "3           6.911131                      3                        3  \n",
       "4          12.518471                      3                        3  "
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 9. 序列行为特征\n",
    "def gen_mb_sequence_features(df):\n",
    "    \"\"\"\n",
    "    基于访问序列的特征\n",
    "    关注用户访问的连续性、循环性等\n",
    "    \"\"\"\n",
    "    feature = df[[\"CUST_NO\"]].drop_duplicates(['CUST_NO']).copy().reset_index(drop=True)\n",
    "    \n",
    "    # 按时间排序\n",
    "    df_sorted = df.sort_values(['CUST_NO', 'OPERATION_DATE', 'date_days_to_now']).copy()\n",
    "    \n",
    "    # 9.1 页面访问序列特征\n",
    "    # 连续访问同一页面的次数\n",
    "    df_sorted['same_page'] = (df_sorted.groupby('CUST_NO')['PAGE_TITLE'].shift(1) == df_sorted['PAGE_TITLE']).astype(int)\n",
    "    tmp = df_sorted.groupby('CUST_NO')['same_page'].agg(['sum', 'mean'])\n",
    "    tmp.columns = ['same_page_consecutive_count', 'same_page_consecutive_ratio']\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 9.2 模块访问序列特征\n",
    "    # 连续访问同一模块的次数\n",
    "    df_sorted['same_module'] = (df_sorted.groupby('CUST_NO')['MODEL_NAME'].shift(1) == df_sorted['MODEL_NAME']).astype(int)\n",
    "    tmp = df_sorted.groupby('CUST_NO')['same_module'].agg(['sum', 'mean'])\n",
    "    tmp.columns = ['same_module_consecutive_count', 'same_module_consecutive_ratio']\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 9.3 页面回访特征\n",
    "    # 统计用户重复访问同一页面的情况\n",
    "    df_page_revisit = df.groupby(['CUST_NO', 'PAGE_TITLE']).size().reset_index(name='page_visit_count')\n",
    "    \n",
    "    # 有多少页面被访问超过1次\n",
    "    tmp = df_page_revisit[df_page_revisit['page_visit_count'] > 1].groupby('CUST_NO').size().to_frame(\n",
    "        'revisited_page_count'\n",
    "    ).reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 平均每个页面被访问次数\n",
    "    tmp = df_page_revisit.groupby('CUST_NO')['page_visit_count'].mean().to_frame(\n",
    "        'avg_page_visit_count'\n",
    "    ).reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 9.4 访问间隔特征\n",
    "    # 计算相邻两次访问的时间间隔\n",
    "    df_sorted['days_diff'] = df_sorted.groupby('CUST_NO')['date_days_to_now'].diff().abs()\n",
    "    \n",
    "    tmp = df_sorted.groupby('CUST_NO')['days_diff'].agg(['mean', 'max', 'min', 'std'])\n",
    "    tmp.columns = [f'visit_interval_{col}' for col in tmp.columns]\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 9.5 会话特征 (假设间隔>1天为不同会话)\n",
    "    df_sorted['new_session'] = (df_sorted['days_diff'] > 1).astype(int)\n",
    "    df_sorted['session_id'] = df_sorted.groupby('CUST_NO')['new_session'].cumsum()\n",
    "    \n",
    "    # 会话数量\n",
    "    tmp = df_sorted.groupby('CUST_NO')['session_id'].max().to_frame('total_sessions').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 平均每个会话的访问次数\n",
    "    session_counts = df_sorted.groupby(['CUST_NO', 'session_id']).size().reset_index(name='session_count')\n",
    "    tmp = session_counts.groupby('CUST_NO')['session_count'].agg(['mean', 'max', 'std'])\n",
    "    tmp.columns = [f'session_visit_{col}' for col in tmp.columns]\n",
    "    tmp = tmp.reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    # 9.6 最近N次访问的页面/模块\n",
    "    # 最近3次访问是否重复\n",
    "    df_recent = df_sorted.groupby('CUST_NO').tail(3)\n",
    "    tmp = df_recent.groupby('CUST_NO')['PAGE_TITLE'].nunique().to_frame('recent_3_page_nunique').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    tmp = df_recent.groupby('CUST_NO')['MODEL_NAME'].nunique().to_frame('recent_3_module_nunique').reset_index()\n",
    "    feature = feature.merge(tmp, on='CUST_NO', how='left')\n",
    "    \n",
    "    return feature\n",
    "\n",
    "mb_sequence_features = gen_mb_sequence_features(MB_PAGEVIEW_DTL_data)\n",
    "print(f\"✓ 序列行为特征数量: {mb_sequence_features.shape[1] - 1}\")\n",
    "mb_sequence_features.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5a74d365",
   "metadata": {},
   "source": [
    "## 合并所有特征并保存"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "e4d679a0",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================================================================\n",
      "掌银页面访问明细表特征工程完成!\n",
      "================================================================================\n",
      "总特征数量: 502\n",
      "客户数量: 2753\n",
      "\n",
      "各部分特征数量:\n",
      "  1. 基础RFM特征: 18\n",
      "  2. 导航路径特征: 66\n",
      "  3. 页面/模块分组特征: 300\n",
      "  4. 时间段行为特征: 28\n",
      "  5. 用户习惯特征: 24\n",
      "  6. 趋势变化特征: 33\n",
      "  7. 交叉统计特征: 17\n",
      "  8. 序列行为特征: 16\n",
      "================================================================================\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>CUST_NO</th>\n",
       "      <th>max_nunique_days_to_now_0</th>\n",
       "      <th>max_count_days_to_now_0</th>\n",
       "      <th>max_nunique_days_to_now_1</th>\n",
       "      <th>max_count_days_to_now_1</th>\n",
       "      <th>max_nunique_days_to_now_2</th>\n",
       "      <th>max_count_days_to_now_2</th>\n",
       "      <th>daily_page_nunique_mean</th>\n",
       "      <th>daily_page_count_mean</th>\n",
       "      <th>daily_page_nunique_max</th>\n",
       "      <th>...</th>\n",
       "      <th>visit_interval_mean</th>\n",
       "      <th>visit_interval_max</th>\n",
       "      <th>visit_interval_min</th>\n",
       "      <th>visit_interval_std</th>\n",
       "      <th>total_sessions</th>\n",
       "      <th>session_visit_mean</th>\n",
       "      <th>session_visit_max</th>\n",
       "      <th>session_visit_std</th>\n",
       "      <th>recent_3_page_nunique</th>\n",
       "      <th>recent_3_module_nunique</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0004b47a5be96a8379e58932489ca6da</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>61.0</td>\n",
       "      <td>61.0</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>18.0</td>\n",
       "      <td>37.0</td>\n",
       "      <td>18</td>\n",
       "      <td>...</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0</td>\n",
       "      <td>37.0</td>\n",
       "      <td>37</td>\n",
       "      <td>NaN</td>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>00347b0bb271750962f327d2d5bbf737</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>35.0</td>\n",
       "      <td>35.0</td>\n",
       "      <td>69.0</td>\n",
       "      <td>69.0</td>\n",
       "      <td>11.0</td>\n",
       "      <td>15.0</td>\n",
       "      <td>15</td>\n",
       "      <td>...</td>\n",
       "      <td>0.795455</td>\n",
       "      <td>34.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>5.124404</td>\n",
       "      <td>1</td>\n",
       "      <td>22.5</td>\n",
       "      <td>29</td>\n",
       "      <td>9.192388</td>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>00457ecc79efe086078ac66df918d059</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>79.0</td>\n",
       "      <td>79.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>5</td>\n",
       "      <td>...</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.000000</td>\n",
       "      <td>0</td>\n",
       "      <td>5.0</td>\n",
       "      <td>5</td>\n",
       "      <td>NaN</td>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>006a69967d1cddb741798cd43e4a6ddc</td>\n",
       "      <td>6.0</td>\n",
       "      <td>6.0</td>\n",
       "      <td>38.0</td>\n",
       "      <td>46.0</td>\n",
       "      <td>90.0</td>\n",
       "      <td>90.0</td>\n",
       "      <td>8.5</td>\n",
       "      <td>12.5</td>\n",
       "      <td>10</td>\n",
       "      <td>...</td>\n",
       "      <td>1.714286</td>\n",
       "      <td>44.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>7.745967</td>\n",
       "      <td>3</td>\n",
       "      <td>12.5</td>\n",
       "      <td>15</td>\n",
       "      <td>2.380476</td>\n",
       "      <td>3</td>\n",
       "      <td>3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>006d0ffe16adb65bc9afddb229eb6e88</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>NaN</td>\n",
       "      <td>87.0</td>\n",
       "      <td>87.0</td>\n",
       "      <td>2.5</td>\n",
       "      <td>3.0</td>\n",
       "      <td>3</td>\n",
       "      <td>...</td>\n",
       "      <td>1.000000</td>\n",
       "      <td>5.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>2.236068</td>\n",
       "      <td>1</td>\n",
       "      <td>3.0</td>\n",
       "      <td>4</td>\n",
       "      <td>1.414214</td>\n",
       "      <td>3</td>\n",
       "      <td>2</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 503 columns</p>\n",
       "</div>"
      ],
      "text/plain": [
       "                            CUST_NO  max_nunique_days_to_now_0  \\\n",
       "0  0004b47a5be96a8379e58932489ca6da                        NaN   \n",
       "1  00347b0bb271750962f327d2d5bbf737                        NaN   \n",
       "2  00457ecc79efe086078ac66df918d059                        NaN   \n",
       "3  006a69967d1cddb741798cd43e4a6ddc                        6.0   \n",
       "4  006d0ffe16adb65bc9afddb229eb6e88                        NaN   \n",
       "\n",
       "   max_count_days_to_now_0  max_nunique_days_to_now_1  \\\n",
       "0                      NaN                       61.0   \n",
       "1                      NaN                       35.0   \n",
       "2                      NaN                        NaN   \n",
       "3                      6.0                       38.0   \n",
       "4                      NaN                        NaN   \n",
       "\n",
       "   max_count_days_to_now_1  max_nunique_days_to_now_2  \\\n",
       "0                     61.0                        NaN   \n",
       "1                     35.0                       69.0   \n",
       "2                      NaN                       79.0   \n",
       "3                     46.0                       90.0   \n",
       "4                      NaN                       87.0   \n",
       "\n",
       "   max_count_days_to_now_2  daily_page_nunique_mean  daily_page_count_mean  \\\n",
       "0                      NaN                     18.0                   37.0   \n",
       "1                     69.0                     11.0                   15.0   \n",
       "2                     79.0                      5.0                    5.0   \n",
       "3                     90.0                      8.5                   12.5   \n",
       "4                     87.0                      2.5                    3.0   \n",
       "\n",
       "   daily_page_nunique_max  ...  visit_interval_mean  visit_interval_max  \\\n",
       "0                      18  ...             0.000000                 0.0   \n",
       "1                      15  ...             0.795455                34.0   \n",
       "2                       5  ...             0.000000                 0.0   \n",
       "3                      10  ...             1.714286                44.0   \n",
       "4                       3  ...             1.000000                 5.0   \n",
       "\n",
       "   visit_interval_min  visit_interval_std  total_sessions  session_visit_mean  \\\n",
       "0                 0.0            0.000000               0                37.0   \n",
       "1                 0.0            5.124404               1                22.5   \n",
       "2                 0.0            0.000000               0                 5.0   \n",
       "3                 0.0            7.745967               3                12.5   \n",
       "4                 0.0            2.236068               1                 3.0   \n",
       "\n",
       "   session_visit_max  session_visit_std  recent_3_page_nunique  \\\n",
       "0                 37                NaN                      3   \n",
       "1                 29           9.192388                      3   \n",
       "2                  5                NaN                      3   \n",
       "3                 15           2.380476                      3   \n",
       "4                  4           1.414214                      3   \n",
       "\n",
       "   recent_3_module_nunique  \n",
       "0                        2  \n",
       "1                        2  \n",
       "2                        2  \n",
       "3                        3  \n",
       "4                        2  \n",
       "\n",
       "[5 rows x 503 columns]"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 10. 合并所有特征\n",
    "from functools import reduce\n",
    "\n",
    "# 合并所有特征集\n",
    "feature_dfs = [\n",
    "    mb_basic_features,\n",
    "    mb_navigation_features,\n",
    "    mb_page_module_features,\n",
    "    mb_time_period_features,\n",
    "    mb_user_habit_features,\n",
    "    mb_trend_features,\n",
    "    mb_cross_features,\n",
    "    mb_sequence_features\n",
    "]\n",
    "\n",
    "# 使用reduce进行多个DataFrame的合并\n",
    "MB_PAGEVIEW_DTL_features = reduce(lambda left, right: pd.merge(left, right, on='CUST_NO', how='outer'), feature_dfs)\n",
    "\n",
    "print(\"=\"*80)\n",
    "print(\"掌银页面访问明细表特征工程完成!\")\n",
    "print(\"=\"*80)\n",
    "print(f\"总特征数量: {MB_PAGEVIEW_DTL_features.shape[1] - 1}\")  # 减去CUST_NO列\n",
    "print(f\"客户数量: {MB_PAGEVIEW_DTL_features.shape[0]}\")\n",
    "print(f\"\\n各部分特征数量:\")\n",
    "print(f\"  1. 基础RFM特征: {mb_basic_features.shape[1] - 1}\")\n",
    "print(f\"  2. 导航路径特征: {mb_navigation_features.shape[1] - 1}\")\n",
    "print(f\"  3. 页面/模块分组特征: {mb_page_module_features.shape[1] - 1}\")\n",
    "print(f\"  4. 时间段行为特征: {mb_time_period_features.shape[1] - 1}\")\n",
    "print(f\"  5. 用户习惯特征: {mb_user_habit_features.shape[1] - 1}\")\n",
    "print(f\"  6. 趋势变化特征: {mb_trend_features.shape[1] - 1}\")\n",
    "print(f\"  7. 交叉统计特征: {mb_cross_features.shape[1] - 1}\")\n",
    "print(f\"  8. 序列行为特征: {mb_sequence_features.shape[1] - 1}\")\n",
    "print(\"=\"*80)\n",
    "\n",
    "# 显示前几行\n",
    "MB_PAGEVIEW_DTL_features.head()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "1a574515",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✅ 特征已保存到: ./feature/mb_pageview_dtl_features.pkl\n",
      "   文件大小: 10.60 MB\n"
     ]
    }
   ],
   "source": [
    "# 检查feature目录\n",
    "feature_dir = 'feature'\n",
    "if not os.path.exists(feature_dir):\n",
    "    os.makedirs(feature_dir)\n",
    "\n",
    "# 保存为pickle格式\n",
    "feature_path = './feature/mb_pageview_dtl_features.pkl'\n",
    "MB_PAGEVIEW_DTL_features.to_pickle(feature_path)\n",
    "print(f\"✅ 特征已保存到: {feature_path}\")\n",
    "print(f\"   文件大小: {os.path.getsize(feature_path) / 1024 / 1024:.2f} MB\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "starcup",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
